Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-notes.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2021-04-22 11:22:11 +0300
committerStefan Niedermann <info@niedermann.it>2021-04-22 11:26:53 +0300
commite3c4c1cb40ba229f50dd534c9edef42cb807ed18 (patch)
treed77940487f9f0714d6f44561d929f6e20d3e3d1d /app/src/main/java
parenta5a84360bea5a9d4d7c755200ce87a8888cfbb2d (diff)
parentc6eed04bfdd3c6d3a8276584239e626b888d7f52 (diff)
Merge branch 'master' into 916-settings
# Conflicts: # app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java # app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java # app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java # app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java33
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java1
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java9
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java3
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java53
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java63
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java7
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java11
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java67
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java213
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java66
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java53
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java66
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java54
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java42
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java14
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java121
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java28
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java959
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java607
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java186
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java171
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java129
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java36
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java72
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java25
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java33
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java37
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java43
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java73
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuItem.java56
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java32
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationAdapter.java94
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationClickListener.java7
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationItem.java96
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationViewHolder.java63
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/slots/SlotterUtil.java74
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslot.java22
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslotter.java53
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java30
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java13
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java84
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java214
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java170
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java529
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java33
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java22
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java22
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java1324
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java346
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java233
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java19
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java58
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CategoryOptionsDao.java42
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java197
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java20
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetSingleNoteDao.java21
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java247
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryOptions.java93
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryWithNotesCount.java53
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Converters.java57
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java251
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/NotesListWidgetData.java89
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/SingleNoteWidgetData.java65
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java15
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java17
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java15
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java21
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java21
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java30
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_16_17.java14
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_17_18.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java18
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_19_20.java30
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_20_21.java232
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java16
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_5_6.java14
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java19
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java28
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java127
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java17
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java33
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java46
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java18
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java33
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java104
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java80
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java20
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ENavigationCategoryType.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java9
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java155
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/NavigationCategory.java76
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java24
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java13
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java55
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java44
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java68
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java193
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java60
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java44
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java46
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java25
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java18
126 files changed, 5635 insertions, 4349 deletions
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
index 980e5e0e..fc4b8fc6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
@@ -8,9 +8,10 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.LiveData;
import it.niedermann.owncloud.notes.main.MainActivity;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
public class AppendToNoteActivity extends MainActivity {
@@ -36,23 +37,25 @@ public class AppendToNoteActivity extends MainActivity {
@Override
public void onNoteClick(int position, View v) {
if (!TextUtils.isEmpty(receivedText)) {
- final DBNote note = db.getNote(localAccount.getId(), ((DBNote) adapter.getItem(position)).getId());
- final String oldContent = note.getContent();
- String newContent;
- if (oldContent != null && oldContent.length() > 0) {
- newContent = oldContent + "\n\n" + receivedText;
- } else {
- newContent = receivedText;
- }
- db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, () -> Toast.makeText(this, getString(R.string.added_content, receivedText), Toast.LENGTH_SHORT).show());
+ final LiveData<Note> fullNote$ = mainViewModel.getFullNote$(((Note) adapter.getItem(position)).getId());
+ fullNote$.observe(this, (fullNote) -> {
+ fullNote$.removeObservers(this);
+ final String oldContent = fullNote.getContent();
+ String newContent;
+ if (oldContent != null && oldContent.length() > 0) {
+ newContent = oldContent + "\n\n" + receivedText;
+ } else {
+ newContent = receivedText;
+ }
+ LiveData<Void> updateLiveData = mainViewModel.updateNoteAndSync(fullNote, newContent, null);
+ updateLiveData.observe(this, (next) -> {
+ Toast.makeText(this, getString(R.string.added_content, receivedText), Toast.LENGTH_SHORT).show();
+ updateLiveData.removeObservers(this);
+ });
+ });
} else {
Toast.makeText(this, R.string.shared_text_empty, Toast.LENGTH_SHORT).show();
}
finish();
}
-
- @Override
- public boolean onNoteLongClick(int position, View v) {
- return false;
- }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java
index 3cfa6d26..4bd65501 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java
@@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
+import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandedActivity;
import it.niedermann.owncloud.notes.databinding.ActivityFormattingHelpBinding;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java
index 7ce7206b..7a58282f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java
@@ -3,14 +3,15 @@ package it.niedermann.owncloud.notes;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
-import android.os.Build;
import android.os.Bundle;
import android.util.Log;
+import android.view.WindowManager;
import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
-import it.niedermann.owncloud.notes.exception.ExceptionHandler;
import it.niedermann.owncloud.notes.branding.BrandedActivity;
+import it.niedermann.owncloud.notes.exception.ExceptionHandler;
public abstract class LockedActivity extends BrandedActivity {
@@ -25,6 +26,10 @@ public abstract class LockedActivity extends BrandedActivity {
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+ if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_prevent_screen_capture), false)) {
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
+ }
+
if (isTaskRoot()) {
askToUnlock();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
index 7d02a1f4..7299bc59 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
@@ -7,14 +7,13 @@ import android.content.res.Configuration;
import android.util.Log;
import androidx.appcompat.app.AppCompatDelegate;
-import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
import static androidx.preference.PreferenceManager.getDefaultSharedPreferences;
-public class NotesApplication extends MultiDexApplication {
+public class NotesApplication extends Application {
private static final String TAG = NotesApplication.class.getSimpleName();
private static final long LOCK_TIME = 30_000;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java
index 91011250..841ae646 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java
@@ -14,27 +14,33 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.RecyclerView;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder;
import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
import it.niedermann.owncloud.notes.databinding.DialogChooseAccountBinding;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.account.AccountChooserAdapter;
import it.niedermann.owncloud.notes.shared.account.AccountChooserViewHolder;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
/**
- * A {@link DialogFragment} which provides an {@link LocalAccount} chooser that hides the given {@link LocalAccount}.
- * This can be useful when one wants to pick e. g. a target for move a note from one {@link LocalAccount} to another..
+ * A {@link DialogFragment} which provides an {@link Account} chooser that hides the current {@link Account}.
+ * This can be useful when one wants to pick e. g. a target for move a {@link Note} from one {@link Account} to another..
*/
public class AccountPickerDialogFragment extends BrandedDialogFragment {
+ private static final String PARAM_TARGET_ACCOUNTS = "targetAccounts";
+ private static final String PARAM_CURRENT_ACCOUNT_ID = "currentAccountId";
+
private AccountPickerListener accountPickerListener;
- private static final String PARAM_ACCOUNT_ID_TO_EXCLUDE = "account_id_to_exclude";
- private long accountIdToExclude;
+
+ private List<Account> targetAccounts;
/**
* Use newInstance()-Method
@@ -50,31 +56,33 @@ public class AccountPickerDialogFragment extends BrandedDialogFragment {
} else {
throw new ClassCastException("Caller must implement " + AccountPickerListener.class.getSimpleName());
}
- accountIdToExclude = requireArguments().getLong(PARAM_ACCOUNT_ID_TO_EXCLUDE, -1L);
- if (accountIdToExclude < 0) {
- throw new IllegalArgumentException(PARAM_ACCOUNT_ID_TO_EXCLUDE + " must be greater 0");
+ final Bundle args = requireArguments();
+ final Collection<?> accounts;
+ if (!args.containsKey(PARAM_TARGET_ACCOUNTS)) {
+ throw new IllegalArgumentException(PARAM_TARGET_ACCOUNTS + " is required.");
+ }
+ accounts = (Collection<?>) args.getSerializable(PARAM_TARGET_ACCOUNTS);
+ if (accounts == null) {
+ throw new IllegalArgumentException(PARAM_TARGET_ACCOUNTS + " is required.");
}
+ long currentAccountId = requireArguments().getLong(PARAM_CURRENT_ACCOUNT_ID, -1L);
+ targetAccounts = accounts
+ .stream()
+ .map(a -> (Account) a)
+ .filter(a -> a.getId() != currentAccountId)
+ .collect(Collectors.toList());
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- final List<LocalAccount> accountsList = NotesDatabase.getInstance(getActivity()).getAccounts();
final AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireActivity())
.setTitle(R.string.simple_move)
.setNegativeButton(android.R.string.cancel, null);
- if (accountsList.size() > 1) {
+ if (targetAccounts.size() > 0) {
final DialogChooseAccountBinding binding = DialogChooseAccountBinding.inflate(LayoutInflater.from(requireContext()));
-
- for (int i = 0; i < accountsList.size(); i++) {
- if (accountsList.get(i).getId() == accountIdToExclude) {
- accountsList.remove(i);
- break;
- }
- }
-
- RecyclerView.Adapter<AccountChooserViewHolder> adapter = new AccountChooserAdapter(accountsList, (account -> {
+ RecyclerView.Adapter<AccountChooserViewHolder> adapter = new AccountChooserAdapter(targetAccounts, (account -> {
accountPickerListener.onAccountPicked(account);
dismiss();
}));
@@ -94,10 +102,11 @@ public class AccountPickerDialogFragment extends BrandedDialogFragment {
return super.onCreateView(inflater, container, savedInstanceState);
}
- public static DialogFragment newInstance(long accountIdToExclude) {
+ public static DialogFragment newInstance(@NonNull ArrayList<Account> targetAccounts, long currentAccountId) {
final DialogFragment fragment = new AccountPickerDialogFragment();
final Bundle args = new Bundle();
- args.putLong(PARAM_ACCOUNT_ID_TO_EXCLUDE, accountIdToExclude);
+ args.putSerializable(PARAM_TARGET_ACCOUNTS, targetAccounts);
+ args.putLong(PARAM_CURRENT_ACCOUNT_ID, currentAccountId);
fragment.setArguments(args);
return fragment;
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java
index c77e1824..4b77333a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java
@@ -2,8 +2,8 @@ package it.niedermann.owncloud.notes.accountpicker;
import androidx.annotation.NonNull;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public interface AccountPickerListener {
- void onAccountPicked(@NonNull LocalAccount account);
+ void onAccountPicked(@NonNull Account account);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java
index 92e77e74..becc3ac1 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java
@@ -11,16 +11,16 @@ import java.util.ArrayList;
import java.util.List;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class AccountSwitcherAdapter extends RecyclerView.Adapter<AccountSwitcherViewHolder> {
@NonNull
- private final List<LocalAccount> localAccounts = new ArrayList<>();
+ private final List<Account> localAccounts = new ArrayList<>();
@NonNull
- private final Consumer<LocalAccount> onAccountClick;
+ private final Consumer<Account> onAccountClick;
- public AccountSwitcherAdapter(@NonNull Consumer<LocalAccount> onAccountClick) {
+ public AccountSwitcherAdapter(@NonNull Consumer<Account> onAccountClick) {
this.onAccountClick = onAccountClick;
setHasStableIds(true);
}
@@ -46,7 +46,7 @@ public class AccountSwitcherAdapter extends RecyclerView.Adapter<AccountSwitcher
return localAccounts.size();
}
- public void setLocalAccounts(@NonNull List<LocalAccount> localAccounts) {
+ public void setLocalAccounts(@NonNull List<Account> localAccounts) {
this.localAccounts.clear();
this.localAccounts.addAll(localAccounts);
notifyDataSetChanged();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java
index 4286cf1e..79467d86 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java
@@ -10,6 +10,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.LiveData;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
@@ -21,13 +22,12 @@ import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding;
import it.niedermann.owncloud.notes.manageaccounts.ManageAccountsActivity;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.applyBrandToLayerDrawable;
-import static it.niedermann.owncloud.notes.main.MainActivity.manage_account;
/**
- * Displays all available {@link LocalAccount} entries and provides basic operations for them, like adding or switching
+ * Displays all available {@link Account} entries and provides basic operations for them, like adding or switching
*/
public class AccountSwitcherDialog extends BrandedDialogFragment {
@@ -55,7 +55,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
this.currentAccountId = args.getLong(KEY_CURRENT_ACCOUNT_ID);
}
- db = NotesDatabase.getInstance(getActivity());
+ db = NotesDatabase.getInstance(requireActivity());
}
@NonNull
@@ -63,29 +63,36 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
- LocalAccount currentLocalAccount = db.getAccount(currentAccountId);
- binding.accountName.setText(currentLocalAccount.getUserName());
- binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost());
- Glide.with(requireContext())
- .load(currentLocalAccount.getUrl() + "/index.php/avatar/" + Uri.encode(currentLocalAccount.getUserName()) + "/64")
- .error(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.currentAccountItemAvatar);
- binding.accountLayout.setOnClickListener((v) -> dismiss());
-
- AccountSwitcherAdapter adapter = new AccountSwitcherAdapter((localAccount -> {
- accountSwitcherListener.onAccountChosen(localAccount);
- dismiss();
- }));
- binding.accountsList.setAdapter(adapter);
- List<LocalAccount> localAccounts = db.getAccounts();
- for (LocalAccount localAccount : localAccounts) {
- if (localAccount.getId() == currentLocalAccount.getId()) {
- localAccounts.remove(localAccount);
- break;
- }
- }
- adapter.setLocalAccounts(localAccounts);
+ final LiveData<Account> account$ = db.getAccountDao().getAccountById$(currentAccountId);
+ account$.observe(requireActivity(), (currentLocalAccount) -> {
+ account$.removeObservers(requireActivity());
+
+ binding.accountName.setText(currentLocalAccount.getUserName());
+ binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost());
+ Glide.with(requireContext())
+ .load(currentLocalAccount.getUrl() + "/index.php/avatar/" + Uri.encode(currentLocalAccount.getUserName()) + "/64")
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.currentAccountItemAvatar);
+ binding.accountLayout.setOnClickListener((v) -> dismiss());
+
+ final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter((localAccount -> {
+ accountSwitcherListener.onAccountChosen(localAccount);
+ dismiss();
+ }));
+ binding.accountsList.setAdapter(adapter);
+ final LiveData<List<Account>> localAccounts$ = db.getAccountDao().getAccounts$();
+ localAccounts$.observe(requireActivity(), (localAccounts) -> {
+ localAccounts$.removeObservers(requireActivity());
+ for (Account localAccount : localAccounts) {
+ if (localAccount.getId() == currentLocalAccount.getId()) {
+ localAccounts.remove(localAccount);
+ break;
+ }
+ }
+ adapter.setLocalAccounts(localAccounts);
+ });
+ });
binding.addAccount.setOnClickListener((v) -> {
accountSwitcherListener.addAccount();
@@ -93,7 +100,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
});
binding.manageAccounts.setOnClickListener((v) -> {
- requireActivity().startActivityForResult(new Intent(requireContext(), ManageAccountsActivity.class), manage_account);
+ requireActivity().startActivity(new Intent(requireContext(), ManageAccountsActivity.class));
dismiss();
});
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java
index 35750f2f..87491a16 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java
@@ -1,11 +1,11 @@
package it.niedermann.owncloud.notes.accountswitcher;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import androidx.annotation.NonNull;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public interface AccountSwitcherListener {
void addAccount();
- void onAccountChosen(LocalAccount localAccount);
-
- void onAccountDeleted(LocalAccount localAccount);
+ void onAccountChosen(@NonNull Account localAccount);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
index ae57b892..db04e0db 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
@@ -13,7 +13,7 @@ import com.bumptech.glide.request.RequestOptions;
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
@@ -24,7 +24,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
binding = ItemAccountChooseBinding.bind(itemView);
}
- public void bind(@NonNull LocalAccount localAccount, @NonNull Consumer<LocalAccount> onAccountClick) {
+ public void bind(@NonNull Account localAccount, @NonNull Consumer<Account> onAccountClick) {
binding.accountName.setText(localAccount.getUserName());
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
Glide.with(itemView.getContext())
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java
index a9d88c32..55d492d6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java
@@ -17,6 +17,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.niedermann.owncloud.notes.R;
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.readBrandColors;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon;
public abstract class BrandedActivity extends AppCompatActivity implements Branded {
@@ -37,11 +38,7 @@ public abstract class BrandedActivity extends AppCompatActivity implements Brand
getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true);
colorAccent = typedValue.data;
- if (BrandingUtil.isBrandingEnabled(this)) {
- @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(this);
- @ColorInt final int textColor = BrandingUtil.readBrandTextColor(this);
- applyBrand(mainColor, textColor);
- }
+ readBrandColors(this).observe(this, (pair) -> applyBrand(pair.first, pair.second));
}
@Override
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java
index 63eb2a69..57d24adf 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java
@@ -14,11 +14,9 @@ public abstract class BrandedDialogFragment extends DialogFragment implements Br
@Nullable Context context = getContext();
if (context != null) {
- if (BrandingUtil.isBrandingEnabled(context)) {
- @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
- @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
- applyBrand(mainColor, textColor);
- }
+ @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
+ @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
+ applyBrand(mainColor, textColor);
}
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java
index 5752bff8..b134919c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java
@@ -32,7 +32,7 @@ public abstract class BrandedFragment extends Fragment implements Branded {
colorPrimary = typedValue.data;
@Nullable Context context = getContext();
- if (context != null && BrandingUtil.isBrandingEnabled(context)) {
+ if (context != null) {
@ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
@ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
applyBrand(mainColor, textColor);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java
index d2bcd274..7d75a412 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java
@@ -34,13 +34,11 @@ public class BrandedPreferenceCategory extends PreferenceCategory {
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- if (BrandingUtil.isBrandingEnabled(getContext())) {
- final View v = holder.itemView.findViewById(android.R.id.title);
- @Nullable final Context context = getContext();
- if (context != null && v instanceof TextView) {
- @ColorInt final int mainColor = getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context));
- ((TextView) v).setTextColor(mainColor);
- }
+ final View v = holder.itemView.findViewById(android.R.id.title);
+ @Nullable final Context context = getContext();
+ if (context != null && v instanceof TextView) {
+ @ColorInt final int mainColor = getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context));
+ ((TextView) v).setTextColor(mainColor);
}
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java
index aabd3f04..1bb39297 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java
@@ -15,10 +15,8 @@ public class BrandedSnackbar {
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Snackbar.Duration int duration) {
final Snackbar snackbar = Snackbar.make(view, text, duration);
- if (BrandingUtil.isBrandingEnabled(view.getContext())) {
- int color = BrandingUtil.readBrandMainColor(view.getContext());
- snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color);
- }
+ final int color = BrandingUtil.readBrandMainColor(view.getContext());
+ snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color);
return snackbar;
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java
index a3dd233c..d81d3df2 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java
@@ -16,8 +16,6 @@ import androidx.preference.SwitchPreference;
import it.niedermann.owncloud.notes.R;
-import static android.os.Build.VERSION.SDK_INT;
-import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
public class BrandedSwitchPreference extends SwitchPreference implements Branded {
@@ -61,13 +59,8 @@ public class BrandedSwitchPreference extends SwitchPreference implements Branded
@Override
public void applyBrand(@ColorInt int mainColor, @ColorInt int textColor) {
- if (BrandingUtil.isBrandingEnabled(getContext())) {
- this.mainColor = mainColor;
- this.textColor = textColor;
- } else {
- this.mainColor = getContext().getResources().getColor(R.color.defaultBrand);
- this.textColor = Color.WHITE;
- }
+ this.mainColor = mainColor;
+ this.textColor = textColor;
// onBindViewHolder is called after applyBrand, therefore we have to store the given values and apply them later.
applyBrand();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java
index c92cdf1f..bc0faca7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java
@@ -16,8 +16,12 @@ import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
import androidx.preference.PreferenceManager;
+import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
@@ -33,43 +37,68 @@ public class BrandingUtil {
}
- public static boolean isBrandingEnabled(@NonNull Context context) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- return prefs.getBoolean(context.getString(R.string.pref_key_branding), true);
+ public static LiveData<Pair<Integer, Integer>> readBrandColors(@NonNull Context context) {
+ return new BrandingLiveData(context);
+ }
+
+ private static class BrandingLiveData extends MediatorLiveData<Pair<Integer, Integer>> {
+ @ColorInt
+ Integer lastMainColor = null;
+ @ColorInt
+ Integer lastTextColor = null;
+
+ public BrandingLiveData(@NonNull Context context) {
+ addSource(readBrandMainColorLiveData(context), (nextMainColor) -> {
+ lastMainColor = nextMainColor;
+ if (lastTextColor != null) {
+ postValue(new Pair<>(lastMainColor, lastTextColor));
+ }
+ });
+ addSource(readBrandTextColorLiveData(context), (nextTextColor) -> {
+ lastTextColor = nextTextColor;
+ if (lastMainColor != null) {
+ postValue(new Pair<>(lastMainColor, lastTextColor));
+ }
+ });
+ }
+ }
+
+ public static LiveData<Integer> readBrandMainColorLiveData(@NonNull Context context) {
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_main");
+ return new SharedPreferenceIntLiveData(sharedPreferences, pref_key_branding_main, context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
+ }
+
+ public static LiveData<Integer> readBrandTextColorLiveData(@NonNull Context context) {
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_text");
+ return new SharedPreferenceIntLiveData(sharedPreferences, pref_key_branding_text, Color.WHITE);
}
@ColorInt
public static int readBrandMainColor(@NonNull Context context) {
- if (BrandingUtil.isBrandingEnabled(context)) {
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
- Log.v(TAG, "--- Read: shared_preference_theme_main");
- return sharedPreferences.getInt(pref_key_branding_main, context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
- } else {
- return ContextCompat.getColor(context, R.color.defaultBrand);
- }
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_main");
+ return sharedPreferences.getInt(pref_key_branding_main, context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
}
@ColorInt
public static int readBrandTextColor(@NonNull Context context) {
- if (isBrandingEnabled(context)) {
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
- Log.v(TAG, "--- Read: shared_preference_theme_text");
- return sharedPreferences.getInt(pref_key_branding_text, Color.WHITE);
- } else {
- return Color.WHITE;
- }
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_text");
+ return sharedPreferences.getInt(pref_key_branding_text, Color.WHITE);
}
public static void saveBrandColors(@NonNull Context context, @ColorInt int mainColor, @ColorInt int textColor) {
final int previousMainColor = readBrandMainColor(context);
final int previousTextColor = readBrandTextColor(context);
- SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
+ final SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
Log.v(TAG, "--- Write: shared_preference_theme_main" + " | " + mainColor);
Log.v(TAG, "--- Write: shared_preference_theme_text" + " | " + textColor);
editor.putInt(pref_key_branding_main, mainColor);
editor.putInt(pref_key_branding_text, textColor);
editor.apply();
- if (isBrandingEnabled(context) && context instanceof BrandedActivity) {
+ if (context instanceof BrandedActivity) {
if (mainColor != previousMainColor || textColor != previousTextColor) {
final BrandedActivity activity = (BrandedActivity) context;
activity.runOnUiThread(() -> ActivityCompat.recreate(activity));
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
index 5a00e4fd..383d9e1b 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
@@ -16,6 +16,7 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.ScrollView;
+import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -29,6 +30,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
+import java.util.ArrayList;
+import java.util.Calendar;
+
import it.niedermann.android.util.ColorUtil;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment;
@@ -38,12 +42,11 @@ import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.Categor
import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment;
import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.NotesColorUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
@@ -52,6 +55,7 @@ import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcu
import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon;
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
+import static java.lang.Boolean.TRUE;
public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener {
@@ -65,13 +69,12 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
private static final String SAVEDKEY_NOTE = "note";
private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note";
- private LocalAccount localAccount;
- private SingleSignOnAccount ssoAccount;
+ private Account localAccount;
- protected DBNote note;
+ protected Note note;
// TODO do we really need this? The reference to note is currently the same
@Nullable
- private DBNote originalNote;
+ private Note originalNote;
private int originalScrollY;
protected NotesDatabase db;
private NoteFragmentListener listener;
@@ -93,70 +96,59 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- try {
- this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireActivity().getApplicationContext());
- this.localAccount = db.getLocalAccountByAccountName(ssoAccount.name);
-
- if (savedInstanceState == null) {
- long id = requireArguments().getLong(PARAM_NOTE_ID);
- if (id > 0) {
- long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID);
- if (accountId > 0) {
- /* Switch account if account id has been provided */
- this.localAccount = db.getAccount(accountId);
- SingleAccountHelper.setCurrentAccount(requireActivity().getApplicationContext(), localAccount.getAccountName());
- }
- isNew = false;
- note = originalNote = db.getNote(localAccount.getId(), id);
- } else {
- CloudNote cloudNote = (CloudNote) requireArguments().getSerializable(PARAM_NEWNOTE);
- String content = requireArguments().getString(PARAM_CONTENT);
- if (cloudNote == null) {
- if (content == null) {
- throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing.");
- } else {
- note = new DBNote(-1, -1, null, NoteUtil.generateNoteTitle(content), content, false, getString(R.string.category_readonly), null, DBStatus.VOID, -1, "", 0);
+ new Thread(() -> {
+ try {
+ SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext());
+ this.localAccount = db.getAccountDao().getAccountByName(ssoAccount.name);
+
+ if (savedInstanceState == null) {
+ long id = requireArguments().getLong(PARAM_NOTE_ID);
+ if (id > 0) {
+ long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID);
+ if (accountId > 0) {
+ /* Switch account if account id has been provided */
+ this.localAccount = db.getAccountDao().getAccountById(accountId);
+ SingleAccountHelper.setCurrentAccount(requireContext().getApplicationContext(), localAccount.getAccountName());
}
+ isNew = false;
+ note = originalNote = db.getNoteDao().getNoteById(id);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
} else {
- note = db.getNote(localAccount.getId(), db.addNoteAndSync(ssoAccount, localAccount.getId(), cloudNote));
- originalNote = null;
+ Note cloudNote = (Note) requireArguments().getSerializable(PARAM_NEWNOTE);
+ String content = requireArguments().getString(PARAM_CONTENT);
+ if (cloudNote == null) {
+ if (content == null) {
+ throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing.");
+ } else {
+ note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ }
+ } else {
+ note = db.addNote(localAccount.getId(), cloudNote);
+ originalNote = null;
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ }
}
+ } else {
+ note = (Note) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
+ originalNote = (Note) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
}
- } else {
- note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
- originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
}
- setHasOptionsMenu(true);
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- e.printStackTrace();
- }
+ }).start();
+ setHasOptionsMenu(true);
}
@Nullable
protected abstract ScrollView getScrollView();
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- final ScrollView scrollView = getScrollView();
- if (scrollView != null) {
- scrollView.getViewTreeObserver().addOnScrollChangedListener(() -> {
- if (scrollView.getScrollY() > 0) {
- note.setScrollY(scrollView.getScrollY());
- }
- });
- }
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- final ScrollView scrollView = getScrollView();
- if (scrollView != null) {
- this.originalScrollY = note.getScrollY();
- scrollView.post(() -> scrollView.scrollTo(0, originalScrollY));
- }
- }
+ protected abstract void scrollToY(int scrollY);
@Override
public void onResume() {
@@ -198,16 +190,17 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
- MenuItem itemFavorite = menu.findItem(R.id.menu_favorite);
- prepareFavoriteOption(itemFavorite);
+ if (note != null) {
+ prepareFavoriteOption(menu.findItem(R.id.menu_favorite));
- menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 1, 0)) >= 0);
- menu.findItem(R.id.menu_delete).setVisible(!isNew);
+ menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 1, 0)) >= 0);
+ menu.findItem(R.id.menu_delete).setVisible(!isNew);
+ }
}
private void prepareFavoriteOption(MenuItem item) {
- item.setIcon(note.isFavorite() ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp);
- item.setChecked(note.isFavorite());
+ item.setIcon(TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp);
+ item.setChecked(note.getFavorite());
tintMenuIcon(item, colorAccent);
}
@@ -218,19 +211,21 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_cancel) {
- if (originalNote == null) {
- db.deleteNoteAndSync(ssoAccount, note.getId());
- } else {
- db.updateNoteAndSync(ssoAccount, localAccount, originalNote, null, null);
- }
+ new Thread(() -> {
+ if (originalNote == null) {
+ db.deleteNoteAndSync(localAccount, note.getId());
+ } else {
+ db.updateNoteAndSync(localAccount, originalNote, null, null, null);
+ }
+ }).start();
listener.close();
return true;
} else if (itemId == R.id.menu_delete) {
- db.deleteNoteAndSync(ssoAccount, note.getId());
+ db.deleteNoteAndSync(localAccount, note.getId());
listener.close();
return true;
} else if (itemId == R.id.menu_favorite) {
- db.toggleFavorite(ssoAccount, note, null);
+ db.toggleFavoriteAndSync(localAccount, note.getId());
listener.onNoteUpdated(note);
prepareFavoriteOption(item);
return true;
@@ -241,40 +236,32 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
showEditTitleDialog();
return true;
} else if (itemId == R.id.menu_move) {
- AccountPickerDialogFragment.newInstance(note.getAccountId()).show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName());
+ new Thread(() -> {
+ AccountPickerDialogFragment
+ .newInstance(new ArrayList<>(), note.getAccountId())
+ .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName());
+ }).start();
return true;
} else if (itemId == R.id.menu_share) {
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
return false;
} else if (itemId == MENU_ID_PIN) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class);
-
+ final ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class);
if (shortcutManager != null) {
if (shortcutManager.isRequestPinShortcutSupported()) {
- Intent intent = new Intent(getActivity(), EditNoteActivity.class);
- intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
- intent.setAction(ACTION_SHORTCUT);
-
- ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), note.getId() + "")
+ final ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), note.getId() + "")
.setShortLabel(note.getTitle())
- .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), note.isFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
- .setIntent(intent)
+ .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
+ .setIntent(new Intent(getActivity(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()).setAction(ACTION_SHORTCUT))
.build();
- Intent pinnedShortcutCallbackIntent =
- shortcutManager.createShortcutResultIntent(pinShortcutInfo);
-
- PendingIntent successCallback = PendingIntent.getBroadcast(getActivity(), /* request code */ 0,
- pinnedShortcutCallbackIntent, /* flags */ 0);
-
- shortcutManager.requestPinShortcut(pinShortcutInfo,
- successCallback.getIntentSender());
+ shortcutManager.requestPinShortcut(pinShortcutInfo, PendingIntent.getBroadcast(getActivity(), 0, shortcutManager.createShortcutResultIntent(pinShortcutInfo), 0).getIntentSender());
} else {
Log.i(TAG, "RequestPinShortcut is not supported");
}
} else {
- Log.e(TAG, "ShortcutManager is null");
+ Log.e(TAG, ShortcutManager.class.getSimpleName() + " is null");
}
}
@@ -283,9 +270,25 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
return super.onOptionsItemSelected(item);
}
+ @CallSuper
+ protected void onNoteLoaded(Note note) {
+ this.originalScrollY = note.getScrollY();
+ scrollToY(originalScrollY);
+ final ScrollView scrollView = getScrollView();
+ if (scrollView != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ scrollView.setOnScrollChangeListener((View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) -> {
+ if (scrollY > 0) {
+ note.setScrollY(scrollY);
+ }
+ });
+ }
+ }
+ }
+
public void onCloseNote() {
if (!titleModified && originalNote == null && getContent().isEmpty()) {
- db.deleteNoteAndSync(ssoAccount, note.getId());
+ db.deleteNoteAndSync(localAccount, note.getId());
}
}
@@ -297,16 +300,17 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
protected void saveNote(@Nullable ISyncCallback callback) {
Log.d(TAG, "saveData()");
if (note != null) {
- String newContent = getContent();
+ final String newContent = getContent();
if (note.getContent().equals(newContent)) {
if (note.getScrollY() != originalScrollY) {
Log.v(TAG, "... only saving new scroll state, since content did not change");
- db.updateScrollY(note.getId(), note.getScrollY());
+ db.getNoteDao().updateScrollY(note.getId(), note.getScrollY());
} else {
Log.v(TAG, "... not saving, since nothing has changed");
}
} else {
- note = db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, callback);
+ // FIXME requires database queries on main thread!
+ note = db.updateNoteAndSync(localAccount, note, newContent, null, callback);
listener.onNoteUpdated(note);
requireActivity().invalidateOptionsMenu();
}
@@ -350,7 +354,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
@Override
public void onCategoryChosen(String category) {
- db.setCategory(ssoAccount, note, category, null);
+ db.setCategory(localAccount, note.getId(), category);
+ note.setCategory(category);
listener.onNoteUpdated(note);
}
@@ -358,12 +363,14 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public void onTitleEdited(String newTitle) {
titleModified = true;
note.setTitle(newTitle);
- note = db.updateNoteAndSync(ssoAccount, localAccount, note, note.getContent(), newTitle, null);
- listener.onNoteUpdated(note);
+ new Thread(() -> {
+ note = db.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null);
+ requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note));
+ }).start();
}
- public void moveNote(LocalAccount account) {
- db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), note, account.getId());
+ public void moveNote(Account account) {
+ db.moveNoteToAnotherAccount(account, note);
listener.close();
}
@@ -403,6 +410,6 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public interface NoteFragmentListener {
void close();
- void onNoteUpdated(DBNote note);
+ void onNoteUpdated(Note note);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
index 5fe0e5bc..c00db4aa 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
@@ -3,6 +3,7 @@ package it.niedermann.owncloud.notes.edit;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -10,6 +11,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
@@ -27,14 +29,17 @@ import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener;
import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
-import it.niedermann.owncloud.notes.main.MainActivity;
-import it.niedermann.owncloud.notes.shared.model.Category;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
+import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
+
public class EditNoteActivity extends LockedActivity implements BaseNoteFragment.NoteFragmentListener, AccountPickerListener {
private static final String TAG = EditNoteActivity.class.getSimpleName();
@@ -48,6 +53,7 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
public static final String PARAM_CONTENT = "content";
public static final String PARAM_FAVORITE = "favorite";
+ private CategoryViewModel categoryViewModel;
private ActivityEditBinding binding;
private BaseNoteFragment fragment;
@@ -55,7 +61,7 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
+
try {
if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) == null) {
throw new NoCurrentAccountSelectedException();
@@ -66,6 +72,7 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
return;
}
+ categoryViewModel = new ViewModelProvider(this).get(CategoryViewModel.class);
binding = ActivityEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
@@ -171,12 +178,15 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
private void launchNewNote() {
Intent intent = getIntent();
- String category = null;
+ String categoryTitle = "";
boolean favorite = false;
if (intent.hasExtra(PARAM_CATEGORY)) {
- Category categoryPreselection = (Category) Objects.requireNonNull(intent.getSerializableExtra(PARAM_CATEGORY));
- category = categoryPreselection.category;
- favorite = categoryPreselection.favorite != null ? categoryPreselection.favorite : false;
+ final NavigationCategory categoryPreselection = (NavigationCategory) Objects.requireNonNull(intent.getSerializableExtra(PARAM_CATEGORY));
+ final String category = categoryPreselection.getCategory();
+ if(category != null) {
+ categoryTitle = category;
+ }
+ favorite = categoryPreselection.getType() == FAVORITES;
}
String content = "";
@@ -194,7 +204,7 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
if (content == null) {
content = "";
}
- CloudNote newNote = new CloudNote(0, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, favorite, category, null);
+ Note newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
fragment = NoteEditFragment.newInstanceWithNewNote(newNote);
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
}
@@ -231,19 +241,18 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- close();
- return true;
- case R.id.menu_preview:
- launchExistingNote(getAccountId(), getNoteId(), false);
- return true;
- case R.id.menu_edit:
- launchExistingNote(getAccountId(), getNoteId(), true);
- return true;
- default:
- return super.onOptionsItemSelected(item);
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ close();
+ return true;
+ } else if (itemId == R.id.menu_preview) {
+ launchExistingNote(getAccountId(), getNoteId(), false);
+ return true;
+ } else if (itemId == R.id.menu_edit) {
+ launchExistingNote(getAccountId(), getNoteId(), true);
+ return true;
}
+ return super.onOptionsItemSelected(item);
}
@@ -266,24 +275,19 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
}
@Override
- public void onNoteUpdated(DBNote note) {
+ public void onNoteUpdated(Note note) {
if (note != null) {
binding.toolbar.setTitle(note.getTitle());
- if (note.getCategory().isEmpty()) {
+ if (TextUtils.isEmpty(note.getCategory())) {
binding.toolbar.setSubtitle(null);
} else {
binding.toolbar.setSubtitle(NoteUtil.extendCategory(note.getCategory()));
}
- } else {
- // Maybe account is not authenticated -> note == null
- Log.e(TAG, "note is null, start " + MainActivity.class.getSimpleName());
- startActivity(new Intent(this, MainActivity.class));
- finish();
}
}
@Override
- public void onAccountPicked(@NonNull LocalAccount account) {
+ public void onAccountPicked(@NonNull Account account) {
fragment.moveNote(account);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
index 5aca156d..298a6c3f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
@@ -8,6 +8,7 @@ import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.Layout;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.util.TypedValue;
@@ -15,7 +16,6 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.ScrollView;
@@ -27,7 +27,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import static androidx.core.view.ViewCompat.isAttachedToWindow;
@@ -79,6 +79,13 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
}
@Override
+ protected void scrollToY(int y) {
+ if (binding != null) {
+ binding.scrollView.post(() -> binding.scrollView.setScrollY(y));
+ }
+ }
+
+ @Override
protected Layout getLayout() {
binding.editContent.onPreDraw();
return binding.editContent.getLayout();
@@ -125,39 +132,41 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
}
}
};
+ }
- if (note != null) {
- if (note.getContent().isEmpty()) {
- binding.editContent.requestFocus();
+ @Override
+ public void onResume() {
+ super.onResume();
+ binding.editContent.addTextChangedListener(textWatcher);
+ }
- requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ @Override
+ protected void onNoteLoaded(Note note) {
+ super.onNoteLoaded(note);
+ if (TextUtils.isEmpty(note.getContent())) {
+ binding.editContent.post(() -> {
+ binding.editContent.requestFocus();
final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
- imm.showSoftInput(getView(), InputMethodManager.SHOW_IMPLICIT);
+ imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT);
} else {
Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null.");
}
- }
+ });
+ }
- binding.editContent.setMarkdownString(note.getContent());
- binding.editContent.setEnabled(true);
+ binding.editContent.setMarkdownString(note.getContent());
+ binding.editContent.setEnabled(true);
- final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext());
- binding.editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp));
- if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
- binding.editContent.setTypeface(Typeface.MONOSPACE);
- }
+ final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext());
+ binding.editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp));
+ if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
+ binding.editContent.setTypeface(Typeface.MONOSPACE);
}
}
@Override
- public void onResume() {
- super.onResume();
- binding.editContent.addTextChangedListener(textWatcher);
- }
-
- @Override
public void onPause() {
super.onPause();
binding.editContent.removeTextChangedListener(textWatcher);
@@ -237,7 +246,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
return fragment;
}
- public static BaseNoteFragment newInstanceWithNewNote(CloudNote newNote) {
+ public static BaseNoteFragment newInstanceWithNewNote(Note newNote) {
final BaseNoteFragment fragment = new NoteEditFragment();
final Bundle args = new Bundle();
args.putSerializable(PARAM_NEWNOTE, newNote);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
index 0cdd4d40..fca142e9 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
@@ -24,11 +24,11 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
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.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.util.SSOUtil;
import static androidx.core.view.ViewCompat.isAttachedToWindow;
@@ -42,6 +42,11 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
protected FragmentNotePreviewBinding binding;
+ private boolean noteLoaded = false;
+
+ @Nullable
+ private Runnable setScrollY;
+
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
@@ -55,6 +60,17 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
}
@Override
+ protected synchronized void scrollToY(int y) {
+ this.setScrollY = () -> {
+ if (binding != null) {
+ Log.v("SCROLL set (preview) to", y + "");
+ binding.scrollView.post(() -> binding.scrollView.setScrollY(y));
+ }
+ setScrollY = null;
+ };
+ }
+
+ @Override
protected FloatingActionButton getSearchNextButton() {
return binding.searchNext;
}
@@ -82,20 +98,24 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
+ binding.swiperefreshlayout.setOnRefreshListener(this);
registerInternalNoteLinkHandler();
- binding.singleNoteContent.setMarkdownString(note.getContent());
binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance());
- changedText = note.getContent();
-
- db = NotesDatabase.getInstance(requireContext());
- binding.swiperefreshlayout.setOnRefreshListener(this);
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext());
binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp));
if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
binding.singleNoteContent.setTypeface(Typeface.MONOSPACE);
}
+ }
+ @Override
+ protected void onNoteLoaded(Note note) {
+ super.onNoteLoaded(note);
+ noteLoaded = true;
+ registerInternalNoteLinkHandler();
+ changedText = note.getContent();
+ binding.singleNoteContent.setMarkdownString(note.getContent(), setScrollY);
binding.singleNoteContent.getMarkdownString().observe(requireActivity(), (newContent) -> {
changedText = newContent.toString();
saveNote(null);
@@ -105,7 +125,7 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
protected void registerInternalNoteLinkHandler() {
binding.singleNoteContent.registerOnLinkClickCallback((link) -> {
try {
- final long noteLocalId = db.getLocalIdByRemoteId(this.note.getAccountId(), Long.parseLong(link));
+ final long noteLocalId = db.getNoteDao().getLocalIdByRemoteId(this.note.getAccountId(), Long.parseLong(link));
Log.i(TAG, "Found note for remoteId \"" + link + "\" in account \"" + this.note.getAccountId() + "\" with localId + \"" + noteLocalId + "\". Attempt to open " + EditNoteActivity.class.getSimpleName() + " for this note.");
startActivity(new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, noteLocalId));
return true;
@@ -133,20 +153,24 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
@Override
public void onRefresh() {
- if (db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) {
+ if (noteLoaded && db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) {
binding.swiperefreshlayout.setRefreshing(true);
- try {
- SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext());
- db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, () -> {
- note = db.getNote(note.getAccountId(), note.getId());
- changedText = note.getContent();
- binding.singleNoteContent.setMarkdownString(note.getContent());
- binding.swiperefreshlayout.setRefreshing(false);
- });
- db.getNoteServerSyncHelper().scheduleSync(ssoAccount, false);
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- e.printStackTrace();
- }
+ new Thread(() -> {
+ try {
+ final Account account = db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()).name);
+ db.getNoteServerSyncHelper().addCallbackPull(account, () -> new Thread(() -> {
+ note = db.getNoteDao().getNoteById(note.getId());
+ changedText = note.getContent();
+ requireActivity().runOnUiThread(() -> {
+ binding.singleNoteContent.setMarkdownString(note.getContent());
+ binding.swiperefreshlayout.setRefreshing(false);
+ });
+ }).start());
+ db.getNoteServerSyncHelper().scheduleSync(account, false);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
+ }
+ }).start();
} else {
binding.swiperefreshlayout.setRefreshing(false);
Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java
index b5b4f394..4b3bb9ba 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
@@ -18,8 +19,7 @@ import java.util.List;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding;
-import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem;
-import it.niedermann.owncloud.notes.main.NavigationAdapter.NavigationItem;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
public class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -103,7 +103,7 @@ public class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
}
- void setCategoryList(List<CategoryNavigationItem> categories, String currentSearchString) {
+ void setCategoryList(List<NavigationItem.CategoryNavigationItem> categories, @Nullable String currentSearchString) {
this.categories.clear();
this.categories.addAll(categories);
final NavigationItem clearItem = new NavigationItem(clearItemId, context.getString(R.string.no_category), 0, R.drawable.ic_clear_grey_24dp);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java
index a5802e10..648578d4 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java
@@ -2,7 +2,6 @@ package it.niedermann.owncloud.notes.edit.category;
import android.app.Dialog;
import android.content.Context;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@@ -12,9 +11,11 @@ import android.view.WindowManager;
import android.widget.EditText;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
import java.util.List;
@@ -23,8 +24,7 @@ import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder;
import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.databinding.DialogChangeCategoryBinding;
-import it.niedermann.owncloud.notes.main.NavigationAdapter;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
/**
* This {@link DialogFragment} allows for the selection of a category.
@@ -35,13 +35,18 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
private static final String TAG = CategoryDialogFragment.class.getSimpleName();
private static final String STATE_CATEGORY = "category";
+
+ private CategoryViewModel viewModel;
private DialogChangeCategoryBinding binding;
- private NotesDatabase db;
private CategoryDialogListener listener;
+ private CategoryAdapter adapter;
+
private EditText editCategory;
+ private LiveData<List<NavigationItem.CategoryNavigationItem>> categoryLiveData;
+
@Override
public void applyBrand(int mainColor, int textColor) {
BrandingUtil.applyBrandToEditText(mainColor, textColor, binding.search);
@@ -64,8 +69,6 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
private long accountId;
- private CategoryAdapter adapter;
-
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -82,7 +85,12 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
} else {
throw new IllegalArgumentException("Calling activity or target fragment must implement " + CategoryDialogListener.class.getSimpleName());
}
- db = NotesDatabase.getInstance(getActivity());
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.viewModel = new ViewModelProvider(requireActivity()).get(CategoryViewModel.class);
}
@NonNull
@@ -121,7 +129,10 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
});
binding.recyclerView.setAdapter(adapter);
- new LoadCategoriesTask().execute("");
+
+ categoryLiveData = viewModel.getCategories(accountId);
+ categoryLiveData.observe(requireActivity(), categories -> adapter.setCategoryList(categories, binding.search.getText().toString()));
+
editCategory.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -135,7 +146,7 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
@Override
public void afterTextChanged(Editable s) {
- new LoadCategoriesTask().execute(editCategory.getText().toString());
+ viewModel.postSearchTerm(editCategory.getText().toString());
}
});
@@ -167,6 +178,14 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
}
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (categoryLiveData != null) {
+ categoryLiveData.removeObservers(requireActivity());
+ }
+ }
+
public static DialogFragment newInstance(long accountId, String category) {
final DialogFragment categoryFragment = new CategoryDialogFragment();
final Bundle arguments = new Bundle();
@@ -175,19 +194,4 @@ public class CategoryDialogFragment extends BrandedDialogFragment {
categoryFragment.setArguments(arguments);
return categoryFragment;
}
-
- private class LoadCategoriesTask extends AsyncTask<String, Void, List<NavigationAdapter.CategoryNavigationItem>> {
- String currentSearchString;
-
- @Override
- protected List<NavigationAdapter.CategoryNavigationItem> doInBackground(String... searchText) {
- currentSearchString = searchText[0];
- return db.searchCategories(accountId, currentSearchString);
- }
-
- @Override
- protected void onPostExecute(List<NavigationAdapter.CategoryNavigationItem> categories) {
- adapter.setCategoryList(categories, currentSearchString);
- }
- }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java
new file mode 100644
index 00000000..96900a3b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java
@@ -0,0 +1,42 @@
+package it.niedermann.owncloud.notes.edit.category;
+
+import android.app.Application;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem;
+
+public class CategoryViewModel extends AndroidViewModel {
+
+ private final NotesDatabase db;
+
+ @NonNull
+ private final MutableLiveData<String> searchTerm = new MutableLiveData<>("");
+
+ public CategoryViewModel(@NonNull Application application) {
+ super(application);
+ db = NotesDatabase.getInstance(application);
+ }
+
+ public void postSearchTerm(@NonNull String searchTerm) {
+ this.searchTerm.postValue(searchTerm);
+ }
+
+ @NonNull
+ public LiveData<List<NavigationItem.CategoryNavigationItem>> getCategories(long accountId) {
+ return switchMap(this.searchTerm, searchTerm ->
+ map(db.getNoteDao().searchCategories$(accountId, TextUtils.isEmpty(searchTerm) ? "%" : "%" + searchTerm + "%"),
+ categories -> convertToCategoryNavigationItem(getApplication(), categories)));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java
index 3b84fbc6..09d3380f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java
@@ -33,8 +33,16 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
final Bundle args = getArguments();
if (args != null) {
final Object throwablesArgument = args.getSerializable(KEY_THROWABLES);
- if (throwablesArgument != null) {
- throwables.addAll((ArrayList<Throwable>) throwablesArgument);
+ if (throwablesArgument instanceof Iterable<?>) {
+ for (Object arg : (Iterable<?>) throwablesArgument) {
+ if (arg instanceof Throwable) {
+ throwables.add((Throwable) arg);
+ } else {
+ throw new IllegalArgumentException("Expected all " + KEY_THROWABLES + " to be instance of " + Throwable.class.getSimpleName());
+ }
+ }
+ } else {
+ throw new IllegalArgumentException(KEY_THROWABLES + " needs to be an " + Iterable.class.getSimpleName() + "<" + Throwable.class.getSimpleName() + ">");
}
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java
new file mode 100644
index 00000000..1cd1f206
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java
@@ -0,0 +1,14 @@
+package it.niedermann.owncloud.notes.exception;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This type of {@link Exception} occurs, when a user has an active internet connection but decided by intention not to use it.
+ * Example: "Sync only on Wi-Fi" is set to <code>true</code>, Wi-Fi is not connected, mobile data is available
+ */
+public class IntendedOfflineException extends Exception {
+
+ public IntendedOfflineException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
new file mode 100644
index 00000000..2695f08a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
@@ -0,0 +1,121 @@
+package it.niedermann.owncloud.notes.importaccount;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
+import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.ui.UiExceptionManager;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.ActivityImportAccountBinding;
+import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
+import it.niedermann.owncloud.notes.exception.ExceptionHandler;
+import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+
+public class ImportAccountActivity extends AppCompatActivity {
+
+ private static final String TAG = ImportAccountActivity.class.getSimpleName();
+ public static final int REQUEST_CODE_IMPORT_ACCOUNT = 1;
+
+ private ImportAccountViewModel importAccountViewModel;
+ private ActivityImportAccountBinding binding;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityImportAccountBinding.inflate(getLayoutInflater());
+ importAccountViewModel = new ViewModelProvider(this).get(ImportAccountViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ binding.welcomeText.setText(getString(R.string.welcome_text, getString(R.string.app_name)));
+ binding.addButton.setOnClickListener((v) -> {
+ binding.addButton.setEnabled(false);
+ try {
+ AccountImporter.pickNewAccount(this);
+ } catch (NextcloudFilesAppNotInstalledException e) {
+ UiExceptionManager.showDialogForException(this, e);
+ Log.w(TAG, "=============================================================");
+ Log.w(TAG, "Nextcloud app is not installed. Cannot choose account");
+ e.printStackTrace();
+ } catch (AndroidGetAccountsPermissionNotGranted e) {
+ binding.addButton.setEnabled(true);
+ AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
+ }
+ });
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ super.onSupportNavigateUp();
+ setResult(RESULT_CANCELED);
+ return true;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ try {
+ AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, ssoAccount -> {
+ runOnUiThread(() -> binding.progressCircular.setVisibility(View.VISIBLE));
+
+ SingleAccountHelper.setCurrentAccount(getApplicationContext(), ssoAccount.name);
+ new Thread(() -> {
+ Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId);
+ try {
+ Log.i(TAG, "Loading capabilities for " + ssoAccount.name);
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null);
+ LiveData<Account> createLiveData = importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities);
+ runOnUiThread(() -> createLiveData.observe(this, (account) -> {
+ if (account != null) {
+ Log.i(TAG, capabilities.toString());
+ BrandingUtil.saveBrandColors(this, capabilities.getColor(), capabilities.getTextColor());
+ setResult(RESULT_OK);
+ finish();
+ } else {
+ binding.addButton.setEnabled(true);
+ ExceptionDialogFragment.newInstance(new IllegalStateException("Created account is null.")).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ }));
+ } catch (Throwable e) {
+ e.printStackTrace();
+ runOnUiThread(() -> {
+ binding.addButton.setEnabled(true);
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
+ }
+ }).start();
+ });
+ } catch (AccountImportCancelledException e) {
+ runOnUiThread(() -> binding.addButton.setEnabled(true));
+ Log.i(TAG, "Account import has been canceled.");
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
new file mode 100644
index 00000000..04b30d8d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
@@ -0,0 +1,28 @@
+package it.niedermann.owncloud.notes.importaccount;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+
+public class ImportAccountViewModel extends AndroidViewModel {
+
+ private static final String TAG = ImportAccountViewModel.class.getSimpleName();
+
+ @NonNull
+ private final NotesDatabase db;
+
+ public ImportAccountViewModel(@NonNull Application application) {
+ super(application);
+ this.db = NotesDatabase.getInstance(application.getApplicationContext());
+ }
+
+ public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
+ return db.addAccount(url, username, accountName, capabilities);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
index 42f891dc..3600b0d6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
@@ -1,15 +1,11 @@
package it.niedermann.owncloud.notes.main;
import android.animation.AnimatorInflater;
-import android.annotation.SuppressLint;
import android.app.SearchManager;
import android.content.Intent;
-import android.database.sqlite.SQLiteException;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
@@ -19,6 +15,7 @@ import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -26,7 +23,10 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.GravityCompat;
-import androidx.core.view.ViewCompat;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
@@ -39,263 +39,308 @@ import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-import java.net.HttpURLConnection;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.Collection;
+import java.util.LinkedList;
-import it.niedermann.owncloud.notes.FormattingHelpActivity;
import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.about.AboutActivity;
import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener;
import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherDialog;
import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherListener;
import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
-import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.databinding.ActivityNotesListViewBinding;
import it.niedermann.owncloud.notes.databinding.DrawerLayoutBinding;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
+import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
-import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem;
-import it.niedermann.owncloud.notes.main.NavigationAdapter.NavigationItem;
+import it.niedermann.owncloud.notes.exception.IntendedOfflineException;
+import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity;
import it.niedermann.owncloud.notes.main.items.ItemAdapter;
import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration;
import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper;
import it.niedermann.owncloud.notes.main.items.section.SectionItemDecoration;
+import it.niedermann.owncloud.notes.main.items.selection.ItemSelectionTracker;
+import it.niedermann.owncloud.notes.main.menu.MenuAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker;
-import it.niedermann.owncloud.notes.persistence.LoadNotesListTask;
-import it.niedermann.owncloud.notes.persistence.LoadNotesListTask.NotesLoadedListener;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.preferences.PreferencesActivity;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
-import it.niedermann.owncloud.notes.shared.model.Category;
import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
-import it.niedermann.owncloud.notes.shared.model.Item;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.O;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive;
import static it.niedermann.owncloud.notes.NotesApplication.isGridViewEnabled;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+import static it.niedermann.owncloud.notes.main.menu.MenuAdapter.SERVER_SETTINGS;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
import static it.niedermann.owncloud.notes.shared.util.SSOUtil.askForNewAccount;
-import static java.util.Arrays.asList;
-public class MainActivity extends LockedActivity implements NoteClickListener, ViewProvider, AccountPickerListener, AccountSwitcherListener, CategoryDialogFragment.CategoryDialogListener {
+public class MainActivity extends LockedActivity implements NoteClickListener, AccountPickerListener, AccountSwitcherListener, CategoryDialogFragment.CategoryDialogListener {
private static final String TAG = MainActivity.class.getSimpleName();
+ protected MainViewModel mainViewModel;
+ private CategoryViewModel categoryViewModel;
+
private boolean gridView = true;
public static final String CREATED_NOTE = "it.niedermann.owncloud.notes.created_notes";
public static final String ADAPTER_KEY_RECENT = "recent";
public static final String ADAPTER_KEY_STARRED = "starred";
- public static final String ACTION_FAVORITES = "it.niedermann.owncloud.notes.favorites";
- public static final String ACTION_RECENT = "it.niedermann.owncloud.notes.recent";
-
- private static final String SAVED_STATE_NAVIGATION_SELECTION = "navigationSelection";
- private static final String SAVED_STATE_NAVIGATION_ADAPTER_SLECTION = "navigationAdapterSelection";
- private static final String SAVED_STATE_NAVIGATION_OPEN = "navigationOpen";
+ public static final String ADAPTER_KEY_UNCATEGORIZED = "uncategorized";
private final static int create_note_cmd = 0;
private final static int show_single_note_cmd = 1;
- private final static int server_settings = 2;
- private final static int about = 3;
- public final static int manage_account = 4;
- /**
- * Used to detect the onResume() call after the import dialog has been displayed.
- * https://github.com/stefan-niedermann/nextcloud-notes/pull/599/commits/f40eab402d122f113020200751894fa39c8b9fcc#r334239634
- */
- private boolean notAuthorizedAccountHandled = false;
+ protected ItemAdapter adapter;
+ private NavigationAdapter adapterCategories;
+ private MenuAdapter menuAdapter;
- protected SingleSignOnAccount ssoAccount;
- protected LocalAccount localAccount;
+ private SelectionTracker<Long> tracker;
+ private NotesListViewItemTouchHelper itemTouchHelper;
protected DrawerLayoutBinding binding;
protected ActivityNotesListViewBinding activityBinding;
-
+ protected FloatingActionButton fabCreate;
private CoordinatorLayout coordinatorLayout;
private SwipeRefreshLayout swipeRefreshLayout;
- protected FloatingActionButton fabCreate;
private RecyclerView listView;
+ private ActionMode mActionMode;
- protected ItemAdapter adapter;
-
- protected NotesDatabase db = null;
- private NavigationAdapter adapterCategories;
- private NavigationItem itemRecent;
- private NavigationItem itemFavorites;
- private NavigationItem itemUncategorized;
- @NonNull
- private Category navigationSelection = new Category(null, null);
- private String navigationOpen = "";
boolean canMoveNoteToAnotherAccounts = false;
- private ActionMode mActionMode;
- private final ISyncCallback syncCallBack = () -> {
- adapter.clearSelection(listView);
- if (mActionMode != null) {
- mActionMode.finish();
- }
- refreshLists();
- swipeRefreshLayout.setRefreshing(false);
- };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ categoryViewModel = new ViewModelProvider(this).get(CategoryViewModel.class);
CapabilitiesWorker.update(this);
binding = DrawerLayoutBinding.inflate(getLayoutInflater());
activityBinding = ActivityNotesListViewBinding.bind(binding.activityNotesListView.getRoot());
setContentView(binding.getRoot());
+
this.coordinatorLayout = binding.activityNotesListView.activityNotesListView;
this.swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout;
this.fabCreate = binding.activityNotesListView.fabCreate;
this.listView = binding.activityNotesListView.recyclerView;
- String categoryAdapterSelectedItem = ADAPTER_KEY_RECENT;
- if (savedInstanceState == null) {
- if (ACTION_RECENT.equals(getIntent().getAction())) {
- categoryAdapterSelectedItem = ADAPTER_KEY_RECENT;
- } else if (ACTION_FAVORITES.equals(getIntent().getAction())) {
- categoryAdapterSelectedItem = ADAPTER_KEY_STARRED;
- navigationSelection = new Category(null, true);
- }
- } else {
- Object savedCategory = savedInstanceState.getSerializable(SAVED_STATE_NAVIGATION_SELECTION);
- if (savedCategory != null) {
- navigationSelection = (Category) savedCategory;
- }
- navigationOpen = savedInstanceState.getString(SAVED_STATE_NAVIGATION_OPEN);
- categoryAdapterSelectedItem = savedInstanceState.getString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION);
- }
-
- db = NotesDatabase.getInstance(this);
-
gridView = isGridViewEnabled();
+
if (!gridView || isDarkThemeActive(this)) {
activityBinding.activityNotesListView.setBackgroundColor(ContextCompat.getColor(this, R.color.primary));
}
setupToolbars();
- setupNavigationList(categoryAdapterSelectedItem);
- setupNavigationMenu();
+ setupNavigationList();
setupNotesList();
- new Thread(() -> canMoveNoteToAnotherAccounts = db.getAccountsCount() > 1).start();
- }
-
- @Override
- protected void onResume() {
- try {
- ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext());
- if (localAccount == null || !localAccount.getAccountName().equals(ssoAccount.name)) {
- selectAccount(ssoAccount.name);
+ mainViewModel.getAccountsCount().observe(this, (count) -> {
+ if (count == 0) {
+ startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ } else {
+ new Thread(() -> {
+ try {
+ final Account account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
+ runOnUiThread(() -> mainViewModel.postCurrentAccount(account));
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+ }).start();
}
- } catch (NoCurrentAccountSelectedException | NextcloudFilesAppAccountNotFoundException e) {
- if (localAccount == null) {
- List<LocalAccount> localAccounts = db.getAccounts();
- if (localAccounts.size() > 0) {
- localAccount = localAccounts.get(0);
+ });
+
+ mainViewModel.hasMultipleAccountsConfigured().observe(this, (hasMultipleAccountsConfigured) -> canMoveNoteToAnotherAccounts = hasMultipleAccountsConfigured);
+ mainViewModel.getSyncStatus().observe(this, (syncStatus) -> swipeRefreshLayout.setRefreshing(syncStatus));
+ mainViewModel.getSyncErrors().observe(this, (exceptions) -> BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(exceptions)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show());
+ mainViewModel.getSelectedCategory().observe(this, (selectedCategory) -> {
+ binding.activityNotesListView.emptyContentView.getRoot().setVisibility(GONE);
+ adapter.setShowCategory(selectedCategory.getType() == RECENT || selectedCategory.getType() == FAVORITES);
+ fabCreate.show();
+
+ switch (selectedCategory.getType()) {
+ case RECENT: {
+ activityBinding.searchText.setText(getString(R.string.search_in_all));
+ break;
+ }
+ case FAVORITES: {
+ activityBinding.searchText.setText(getString(R.string.search_in_category, getString(R.string.label_favorites)));
+ break;
+ }
+ case UNCATEGORIZED: {
+ activityBinding.searchText.setText(getString(R.string.search_in_category, getString(R.string.action_uncategorized)));
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category == null) {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ activityBinding.searchText.setText(getString(R.string.search_in_category, NoteUtil.extendCategory(category)));
+ break;
}
}
- if (!notAuthorizedAccountHandled) {
- handleNotAuthorizedAccount();
+
+ fabCreate.setOnClickListener((View view) -> {
+ Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class);
+ createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, selectedCategory);
+ if (activityBinding.searchView.getQuery().length() > 0) {
+ createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString());
+ invalidateOptionsMenu();
+ }
+ startActivityForResult(createIntent, create_note_cmd);
+ });
+ });
+ mainViewModel.getNotesListLiveData().observe(this, notes -> {
+ // https://stackoverflow.com/a/37342327
+ itemTouchHelper.attachToRecyclerView(null);
+ itemTouchHelper.attachToRecyclerView(listView);
+ adapter.setItemList(notes);
+ binding.activityNotesListView.progressCircular.setVisibility(GONE);
+ binding.activityNotesListView.emptyContentView.getRoot().setVisibility(notes.size() > 0 ? GONE : VISIBLE);
+ // Remove deleted notes from the selection
+ if (tracker.hasSelection()) {
+ final Collection<Long> deletedNotes = new LinkedList<>();
+ for (Long id : tracker.getSelection()) {
+ if (notes
+ .stream()
+ .filter(item -> !item.isSection())
+ .map(item -> (Note) item)
+ .noneMatch(item -> item.getId() == id)) {
+ deletedNotes.add(id);
+ }
+ }
+ for (Long id : deletedNotes) {
+ tracker.deselect(id);
+ }
}
- }
+ });
+ mainViewModel.getSearchTerm().observe(this, adapter::setHighlightSearchQuery);
+ mainViewModel.getCategorySortingMethodOfSelectedCategory().observe(this, methodOfCategory -> {
+ updateSortMethodIcon(methodOfCategory.second);
+ activityBinding.sortingMethod.setOnClickListener((v) -> {
+ if (methodOfCategory.first != null) {
+ CategorySortingMethod newMethod = methodOfCategory.second;
+ if (newMethod == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) {
+ newMethod = CategorySortingMethod.SORT_MODIFIED_DESC;
+ } else {
+ newMethod = CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC;
+ }
+ final LiveData<Void> modifyLiveData = mainViewModel.modifyCategoryOrder(methodOfCategory.first, newMethod);
+ modifyLiveData.observe(this, (next) -> modifyLiveData.removeObservers(this));
+ }
+ });
+ });
+ mainViewModel.getNavigationCategories().observe(this, navigationItems -> this.adapterCategories.setItems(navigationItems));
+ mainViewModel.getCurrentAccount().observe(this, (nextAccount) -> {
+ fabCreate.hide();
+ Glide
+ .with(this)
+ .load(nextAccount.getUrl() + "/index.php/avatar/" + Uri.encode(nextAccount.getUserName()) + "/64")
+ .placeholder(R.drawable.ic_account_circle_grey_24dp)
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(activityBinding.launchAccountSwitcher);
- // refresh and sync every time the activity gets
- refreshLists();
- if (localAccount != null) {
- synchronize();
- db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, syncCallBack);
- }
- super.onResume();
+ mainViewModel.synchronizeNotes(nextAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess() {
+ Log.d(TAG, "Successfully synchronized notes for " + nextAccount.getAccountName());
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ if (t.getClass() == IntendedOfflineException.class || t instanceof IntendedOfflineException) {
+ Log.i(TAG, "Capabilities and notes not updated because " + nextAccount.getAccountName() + " is offline by intention.");
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ }
+ });
+ }
+ });
+ fabCreate.show();
+ activityBinding.launchAccountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance(nextAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));
+
+ if (menuAdapter == null) {
+ menuAdapter = new MenuAdapter(getApplicationContext(), nextAccount, (menuItem) -> {
+ @Nullable Integer resultCode = menuItem.getResultCode();
+ if (resultCode == null) {
+ startActivity(menuItem.getIntent());
+ } else {
+ startActivityForResult(menuItem.getIntent(), menuItem.getResultCode());
+ }
+ });
+
+ binding.navigationMenu.setAdapter(menuAdapter);
+ } else {
+ menuAdapter.updateAccount(nextAccount);
+ }
+ });
}
@Override
- protected void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
- if (localAccount != null) {
- outState.putSerializable(SAVED_STATE_NAVIGATION_SELECTION, navigationSelection);
- outState.putString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION, adapterCategories.getSelectedItem());
- outState.putString(SAVED_STATE_NAVIGATION_OPEN, navigationOpen);
- }
- }
+ protected void onResume() {
+ final LiveData<Account> accountLiveData = mainViewModel.getCurrentAccount();
+ accountLiveData.observe(this, (currentAccount) -> {
+ accountLiveData.removeObservers(this);
+ mainViewModel.synchronizeNotes(currentAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess() {
+ Log.d(TAG, "Successfully synchronized notes for " + currentAccount.getAccountName());
+ }
- private void selectAccount(String accountName) {
- fabCreate.hide();
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), accountName);
- localAccount = db.getLocalAccountByAccountName(accountName);
- if (localAccount != null) {
- try {
- BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor());
- ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext());
- new NotesListViewItemTouchHelper(ssoAccount, this, db, adapter, syncCallBack, this::refreshLists, swipeRefreshLayout, this, gridView)
- .attachToRecyclerView(listView);
- synchronize();
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- Log.i(TAG, "Tried to select account, but got an " + e.getClass().getSimpleName() + ". Asking for importing an account...");
- handleNotAuthorizedAccount();
- }
- refreshLists();
- fabCreate.show();
- activityBinding.launchAccountSwitcher.setOnClickListener((v) -> {
- if (localAccount == null) {
- handleNotAuthorizedAccount();
- } else {
- AccountSwitcherDialog.newInstance(localAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName());
+ @Override
+ public void onError(@NonNull Throwable t) {
+ t.printStackTrace();
}
});
- setupNavigationList(ADAPTER_KEY_RECENT);
- } else {
- if (!notAuthorizedAccountHandled) {
- handleNotAuthorizedAccount();
- }
- binding.navigationList.setAdapter(null);
- }
- updateCurrentAccountAvatar();
+ });
+ super.onResume();
}
- private void handleNotAuthorizedAccount() {
- fabCreate.hide();
- swipeRefreshLayout.setRefreshing(false);
- askForNewAccount(this);
- notAuthorizedAccountHandled = true;
+ @Override
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mainViewModel.restoreInstanceState();
}
private void setupToolbars() {
setSupportActionBar(binding.activityNotesListView.toolbar);
- updateCurrentAccountAvatar();
activityBinding.homeToolbar.setOnClickListener((v) -> {
if (activityBinding.toolbar.getVisibility() == GONE) {
updateToolbars(false);
}
});
- activityBinding.launchAccountSwitcher.setOnClickListener((v) -> askForNewAccount(this));
activityBinding.menuButton.setOnClickListener((v) -> binding.drawerLayout.openDrawer(GravityCompat.START));
- final LinearLayout searchEditFrame = activityBinding.searchView.findViewById(R.id
- .search_edit_frame);
+ final LinearLayout searchEditFrame = activityBinding.searchView.findViewById(R.id.search_edit_frame);
searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
int oldVisibility = -1;
@@ -331,16 +376,39 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
@Override
public boolean onQueryTextChange(String newText) {
- refreshLists();
+ mainViewModel.postSearchTerm(newText);
return true;
}
});
}
private void setupNotesList() {
- initRecyclerView();
+ adapter = new ItemAdapter(this, gridView);
+ listView.setAdapter(adapter);
+ listView.setItemAnimator(null);
+ if (gridView) {
+ int spanCount = getResources().getInteger(R.integer.grid_view_span_count);
+ StaggeredGridLayoutManager gridLayoutManager = new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL);
+ listView.setLayoutManager(gridLayoutManager);
+ listView.addItemDecoration(new GridItemDecoration(adapter, spanCount,
+ getResources().getDimensionPixelSize(R.dimen.spacer_3x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_5x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_3x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_1x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x)
+ ));
+ } else {
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ listView.setLayoutManager(layoutManager);
+ listView.addItemDecoration(new SectionItemDecoration(adapter,
+ getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x) + getResources().getDimensionPixelSize(R.dimen.spacer_3x) + getResources().getDimensionPixelSize(R.dimen.spacer_2x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_5x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_1x),
+ 0
+ ));
+ }
- ((RecyclerView) findViewById(R.id.recycler_view)).addOnScrollListener(new RecyclerView.OnScrollListener() {
+ listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0)
@@ -351,74 +419,65 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
});
swipeRefreshLayout.setOnRefreshListener(() -> {
- if (ssoAccount == null) {
- swipeRefreshLayout.setRefreshing(false);
- askForNewAccount(this);
- } else {
- Log.i(TAG, "Clearing Glide memory cache");
- Glide.get(this).clearMemory();
- new Thread(() -> {
- Log.i(TAG, "Clearing Glide disk cache");
- Glide.get(getApplicationContext()).clearDiskCache();
- }).start();
- new Thread(() -> {
- Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
- final Capabilities capabilities;
- try {
- capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, localAccount.getCapabilitiesETag());
- db.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
- db.updateBrand(localAccount.getId(), capabilities);
- db.updateBrand(localAccount.getId(), capabilities);
- localAccount.setColor(Color.parseColor(capabilities.getColor()));
- localAccount.setTextColor(Color.parseColor(capabilities.getTextColor()));
- BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor());
- db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
- Log.i(TAG, capabilities.toString());
- } catch (Exception e) {
- if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
- Log.i(TAG, "Capabilities not modified.");
- } else {
- e.printStackTrace();
- }
- } finally {
- // Even if the capabilities endpoint makes trouble, we can still try to synchronize the notes
- synchronize();
+ Log.i(TAG, "Clearing Glide memory cache");
+ Glide.get(this).clearMemory();
+ new Thread(() -> {
+ Log.i(TAG, "Clearing Glide disk cache");
+ Glide.get(getApplicationContext()).clearDiskCache();
+ }, "CLEAR_GLIDE_CACHE").start();
+ final LiveData<Account> syncLiveData = mainViewModel.getCurrentAccount();
+ final Observer<Account> syncObserver = currentAccount -> {
+ syncLiveData.removeObservers(this);
+ mainViewModel.synchronizeCapabilitiesAndNotes(currentAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess() {
+ Log.d(TAG, "Successfully synchronized capabilities and notes for " + currentAccount.getAccountName());
}
- }).start();
- }
- });
- // Floating Action Button
- fabCreate.setOnClickListener((View view) -> {
- Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class);
- createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, navigationSelection);
- if (activityBinding.searchView.getQuery().length() > 0) {
- createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString());
- invalidateOptionsMenu();
- }
- startActivityForResult(createIntent, create_note_cmd);
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ swipeRefreshLayout.setRefreshing(false);
+ if (t.getClass() == IntendedOfflineException.class || t instanceof IntendedOfflineException) {
+ Log.i(TAG, "Capabilities and notes not updated because " + currentAccount.getAccountName() + " is offline by intention.");
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ }
+ });
+ }
+ });
+ };
+ syncLiveData.observe(this, syncObserver);
});
- activityBinding.sortingMethod.setOnClickListener((v) -> {
- CategorySortingMethod method;
-
- method = db.getCategoryOrder(localAccount.getId(), navigationSelection);
+ tracker = ItemSelectionTracker.build(listView, adapter);
+ adapter.setTracker(tracker);
+ tracker.addObserver(new SelectionTracker.SelectionObserver<Long>() {
+ @Override
+ public void onSelectionChanged() {
+ super.onSelectionChanged();
+ if (tracker.hasSelection() && mActionMode == null) {
+ mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this, coordinatorLayout, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
+ }
+ if (mActionMode != null) {
+ if (tracker.hasSelection()) {
+ int selected = tracker.getSelection().size();
+ mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, selected, selected));
+ } else {
+ mActionMode.finish();
+ mActionMode = null;
+ }
+ }
+ }
+ }
+ );
- if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) {
- method = CategorySortingMethod.SORT_MODIFIED_DESC;
- } else {
- method = CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC;
- }
- db.modifyCategoryOrder(localAccount.getId(), navigationSelection, method);
- refreshLists();
- updateSortMethodIcon(localAccount.getId());
- });
+ itemTouchHelper = new NotesListViewItemTouchHelper(this, mainViewModel, this, tracker, adapter, swipeRefreshLayout, coordinatorLayout, gridView);
+ itemTouchHelper.attachToRecyclerView(listView);
}
- private void setupNavigationList(final String selectedItem) {
- itemRecent = new NavigationItem(ADAPTER_KEY_RECENT, getString(R.string.label_all_notes), null, R.drawable.ic_access_time_grey600_24dp);
- itemFavorites = new NavigationItem(ADAPTER_KEY_STARRED, getString(R.string.label_favorites), null, R.drawable.ic_star_yellow_24dp);
- adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() {
+ private void setupNavigationList() {
+ adapterCategories = new NavigationAdapter(this, new NavigationClickListener() {
@Override
public void onItemClick(NavigationItem item) {
selectItem(item, true);
@@ -427,55 +486,59 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
private void selectItem(NavigationItem item, boolean closeNavigation) {
adapterCategories.setSelectedItem(item.id);
// update current selection
- if (itemRecent.equals(item)) {
- navigationSelection = new Category(null, null);
- } else if (itemFavorites.equals(item)) {
- navigationSelection = new Category(null, true);
- } else if (itemUncategorized != null && itemUncategorized.equals(item)) {
- navigationSelection = new Category("", null);
- } else {
- navigationSelection = new Category(item.label, null);
- }
-
- // auto-close sub-folder in Navigation if selection is outside of that folder
- if (navigationOpen != null) {
- int slashIndex = navigationSelection.category == null ? -1 : navigationSelection.category.indexOf('/');
- String rootCategory = slashIndex < 0 ? navigationSelection.category : navigationSelection.category.substring(0, slashIndex);
- if (!navigationOpen.equals(rootCategory)) {
- navigationOpen = null;
+ if (item.type != null) {
+ switch (item.type) {
+ case RECENT: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(RECENT));
+ break;
+ }
+ case FAVORITES: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(FAVORITES));
+ break;
+ }
+ case UNCATEGORIZED: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(UNCATEGORIZED));
+ break;
+ }
+ default: {
+ if (item.getClass() == NavigationItem.CategoryNavigationItem.class) {
+ mainViewModel.postSelectedCategory(new NavigationCategory(((NavigationItem.CategoryNavigationItem) item).accountId, ((NavigationItem.CategoryNavigationItem) item).category));
+ } else {
+ throw new IllegalStateException(NavigationItem.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but item is not of type " + NavigationItem.CategoryNavigationItem.class.getSimpleName() + ".");
+ }
+ }
}
+ } else {
+ Log.e(TAG, "Unknown item navigation type. Fallback to show " + RECENT);
+ mainViewModel.postSelectedCategory(new NavigationCategory(RECENT));
}
- // update views
if (closeNavigation) {
binding.drawerLayout.closeDrawer(GravityCompat.START);
}
- refreshLists(true);
}
@Override
public void onIconClick(NavigationItem item) {
- if (item.icon == NavigationAdapter.ICON_MULTIPLE && !item.label.equals(navigationOpen)) {
- navigationOpen = item.label;
- selectItem(item, false);
- } else if (item.icon == NavigationAdapter.ICON_MULTIPLE || item.icon == NavigationAdapter.ICON_MULTIPLE_OPEN && item.label.equals(navigationOpen)) {
- navigationOpen = null;
- refreshLists();
- } else {
- onItemClick(item);
- }
+ final LiveData<String> expandedCategoryLiveData = mainViewModel.getExpandedCategory();
+ expandedCategoryLiveData.observe(MainActivity.this, expandedCategory -> {
+ if (item.icon == NavigationAdapter.ICON_MULTIPLE && !item.label.equals(expandedCategory)) {
+ mainViewModel.postExpandedCategory(item.label);
+ selectItem(item, false);
+ } else if (item.icon == NavigationAdapter.ICON_MULTIPLE || item.icon == NavigationAdapter.ICON_MULTIPLE_OPEN && item.label.equals(expandedCategory)) {
+ mainViewModel.postExpandedCategory(null);
+ } else {
+ onItemClick(item);
+ }
+ expandedCategoryLiveData.removeObservers(MainActivity.this);
+ });
}
});
- adapterCategories.setSelectedItem(selectedItem);
+ adapterCategories.setSelectedItem(ADAPTER_KEY_RECENT);
binding.navigationList.setAdapter(adapterCategories);
}
@Override
- public CoordinatorLayout getView() {
- return this.coordinatorLayout;
- }
-
- @Override
public void applyBrand(int mainColor, int textColor) {
applyBrandToPrimaryToolbar(activityBinding.appBar, activityBinding.toolbar);
applyBrandToFAB(mainColor, textColor, activityBinding.fabCreate);
@@ -504,217 +567,20 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
}
}
- private class LoadCategoryListTask extends AsyncTask<Void, Void, List<NavigationItem>> {
- @Override
- protected List<NavigationItem> doInBackground(Void... voids) {
- if (localAccount == null) {
- return new ArrayList<>();
- }
- List<CategoryNavigationItem> categories = db.getCategories(localAccount.getId());
- if (!categories.isEmpty() && categories.get(0).label.isEmpty()) {
- itemUncategorized = categories.get(0);
- itemUncategorized.label = getString(R.string.action_uncategorized);
- itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER;
- } else {
- itemUncategorized = null;
- }
-
- Map<String, Integer> favorites = db.getFavoritesCount(localAccount.getId());
- //noinspection ConstantConditions
- int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0;
- //noinspection ConstantConditions
- int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0;
- itemFavorites.count = numFavorites;
- itemRecent.count = numFavorites + numNonFavorites;
-
- ArrayList<NavigationItem> items = new ArrayList<>();
- items.add(itemRecent);
- items.add(itemFavorites);
- NavigationItem lastPrimaryCategory = null;
- NavigationItem lastSecondaryCategory = null;
- for (NavigationItem item : categories) {
- int slashIndex = item.label.indexOf('/');
- String currentPrimaryCategory = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
- String currentSecondaryCategory = null;
- boolean isCategoryOpen = currentPrimaryCategory.equals(navigationOpen);
-
- if (isCategoryOpen && !currentPrimaryCategory.equals(item.label)) {
- String currentCategorySuffix = item.label.substring(navigationOpen.length() + 1);
- int subSlashIndex = currentCategorySuffix.indexOf('/');
- currentSecondaryCategory = subSlashIndex < 0 ? currentCategorySuffix : currentCategorySuffix.substring(0, subSlashIndex);
- }
-
- boolean belongsToLastPrimaryCategory = lastPrimaryCategory != null && currentPrimaryCategory.equals(lastPrimaryCategory.label);
- boolean belongsToLastSecondaryCategory = belongsToLastPrimaryCategory && lastSecondaryCategory != null && lastSecondaryCategory.label.equals(currentPrimaryCategory + "/" + currentSecondaryCategory);
-
- if (isCategoryOpen && !belongsToLastPrimaryCategory && currentSecondaryCategory != null) {
- lastPrimaryCategory = new NavigationItem("category:" + currentPrimaryCategory, currentPrimaryCategory, 0, NavigationAdapter.ICON_MULTIPLE_OPEN);
- items.add(lastPrimaryCategory);
- belongsToLastPrimaryCategory = true;
- }
-
- if (belongsToLastPrimaryCategory && belongsToLastSecondaryCategory) {
- lastSecondaryCategory.count += item.count;
- lastSecondaryCategory.icon = NavigationAdapter.ICON_SUB_MULTIPLE;
- } else if (belongsToLastPrimaryCategory) {
- if (isCategoryOpen) {
- item.label = currentPrimaryCategory + "/" + currentSecondaryCategory;
- item.id = "category:" + item.label;
- item.icon = NavigationAdapter.ICON_SUB_FOLDER;
- items.add(item);
- lastSecondaryCategory = item;
- } else {
- lastPrimaryCategory.count += item.count;
- lastPrimaryCategory.icon = NavigationAdapter.ICON_MULTIPLE;
- lastSecondaryCategory = null;
- }
- } else {
- if (isCategoryOpen) {
- item.icon = NavigationAdapter.ICON_MULTIPLE_OPEN;
- } else {
- item.label = currentPrimaryCategory;
- item.id = "category:" + item.label;
- }
- items.add(item);
- lastPrimaryCategory = item;
- lastSecondaryCategory = null;
- }
- }
- return items;
- }
-
- @Override
- protected void onPostExecute(List<NavigationItem> items) {
- adapterCategories.setItems(items);
- }
- }
-
- private void setupNavigationMenu() {
- final NavigationItem itemFormattingHelp = new NavigationItem("formattingHelp", getString(R.string.action_formatting_help), null, R.drawable.ic_baseline_help_outline_24);
- final NavigationItem itemTrashbin = new NavigationItem("trashbin", getString(R.string.action_trashbin), null, R.drawable.ic_delete_grey600_24dp);
- final NavigationItem itemSettings = new NavigationItem("settings", getString(R.string.action_settings), null, R.drawable.ic_settings_grey600_24dp);
- final NavigationItem itemAbout = new NavigationItem("about", getString(R.string.simple_about), null, R.drawable.ic_info_outline_grey600_24dp);
-
- NavigationAdapter adapterMenu = new NavigationAdapter(this, new NavigationAdapter.ClickListener() {
- @Override
- public void onItemClick(NavigationItem item) {
- if (itemFormattingHelp.equals(item)) {
- Intent formattingHelpIntent = new Intent(getApplicationContext(), FormattingHelpActivity.class);
- startActivity(formattingHelpIntent);
- } else if (itemSettings.equals(item)) {
- Intent settingsIntent = new Intent(getApplicationContext(), PreferencesActivity.class);
- startActivityForResult(settingsIntent, server_settings);
- } else if (itemAbout.equals(item)) {
- Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
- startActivityForResult(aboutIntent, about);
- } else if (itemTrashbin.equals(item) && localAccount != null) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(localAccount.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin")));
- }
- }
-
- @Override
- public void onIconClick(NavigationItem item) {
- onItemClick(item);
- }
- });
- adapterMenu.setItems(asList(itemFormattingHelp, itemTrashbin, itemSettings, itemAbout));
- binding.navigationMenu.setAdapter(adapterMenu);
- }
-
- private void initRecyclerView() {
- adapter = new ItemAdapter(this, gridView);
- listView.setAdapter(adapter);
-
- if (gridView) {
- int spanCount = getResources().getInteger(R.integer.grid_view_span_count);
- StaggeredGridLayoutManager gridLayoutManager = new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL);
- listView.setLayoutManager(gridLayoutManager);
- listView.addItemDecoration(new GridItemDecoration(adapter, spanCount,
- getResources().getDimensionPixelSize(R.dimen.spacer_3x),
- getResources().getDimensionPixelSize(R.dimen.spacer_5x),
- getResources().getDimensionPixelSize(R.dimen.spacer_3x),
- getResources().getDimensionPixelSize(R.dimen.spacer_1x),
- getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x)
- ));
- } else {
- LinearLayoutManager layoutManager = new LinearLayoutManager(this);
- listView.setLayoutManager(layoutManager);
- listView.addItemDecoration(new SectionItemDecoration(adapter,
- getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x) + getResources().getDimensionPixelSize(R.dimen.spacer_3x) + getResources().getDimensionPixelSize(R.dimen.spacer_2x),
- getResources().getDimensionPixelSize(R.dimen.spacer_5x),
- getResources().getDimensionPixelSize(R.dimen.spacer_1x),
- 0
- ));
- }
- }
-
- private void refreshLists() {
- refreshLists(false);
- }
-
- private void refreshLists(final boolean scrollToTop) {
- if (localAccount == null) {
- fabCreate.hide();
- adapter.removeAll();
- return;
- }
- View emptyContentView = binding.activityNotesListView.emptyContentView.getRoot();
- emptyContentView.setVisibility(GONE);
- binding.activityNotesListView.progressCircular.setVisibility(VISIBLE);
- fabCreate.show();
- String subtitle;
- if (navigationSelection.category != null) {
- if (navigationSelection.category.isEmpty()) {
- subtitle = getString(R.string.search_in_category, getString(R.string.action_uncategorized));
- } else {
- subtitle = getString(R.string.search_in_category, NoteUtil.extendCategory(navigationSelection.category));
- }
- } else if (navigationSelection.favorite != null && navigationSelection.favorite) {
- subtitle = getString(R.string.search_in_category, getString(R.string.label_favorites));
- } else {
- subtitle = getString(R.string.search_in_all);
- }
- activityBinding.searchText.setText(subtitle);
- CharSequence query = null;
- if (activityBinding.searchView.getQuery().length() != 0) {
- query = activityBinding.searchView.getQuery();
- }
-
- NotesLoadedListener callback = (List<Item> notes, boolean showCategory, CharSequence searchQuery) -> {
- adapter.setShowCategory(showCategory);
- adapter.setHighlightSearchQuery(searchQuery);
- adapter.setItemList(notes);
- binding.activityNotesListView.progressCircular.setVisibility(GONE);
- if (notes.size() > 0) {
- emptyContentView.setVisibility(GONE);
- } else {
- emptyContentView.setVisibility(VISIBLE);
- }
- if (scrollToTop) {
- listView.scrollToPosition(0);
- }
- };
- new LoadNotesListTask(localAccount.getId(), getApplicationContext(), callback, navigationSelection, query).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-
- updateSortMethodIcon(localAccount.getId());
- }
-
/**
* Updates sorting method icon.
*/
- private void updateSortMethodIcon(long localAccountId) {
- CategorySortingMethod method = db.getCategoryOrder(localAccountId, navigationSelection);
+ private void updateSortMethodIcon(CategorySortingMethod method) {
if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) {
activityBinding.sortingMethod.setImageResource(R.drawable.alphabetical_asc);
activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_last_modified));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (SDK_INT >= O) {
activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_last_modified));
}
} else {
activityBinding.sortingMethod.setImageResource(R.drawable.modification_desc);
activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_alphabetically));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (SDK_INT >= O) {
activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_alphabetically));
}
}
@@ -747,38 +613,15 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
switch (requestCode) {
case create_note_cmd: {
- // Make sure the request was successful
- if (resultCode == RESULT_OK) {
- //not need because of db.synchronisation in createActivity
-
- Bundle bundle = data.getExtras();
- if (bundle != null && bundle.containsKey(CREATED_NOTE)) {
- DBNote createdNote = (DBNote) bundle.getSerializable(CREATED_NOTE);
- if (createdNote != null) {
- adapter.add(createdNote);
- } else {
- Log.w(TAG, "createdNote must not be null");
- }
- } else {
- Log.w(TAG, "Provide at least " + CREATED_NOTE);
- }
- }
listView.scrollToPosition(0);
break;
}
- case server_settings: {
+ case SERVER_SETTINGS: {
// Recreate activity completely, because theme switching makes problems when only invalidating the views.
// @see https://github.com/stefan-niedermann/nextcloud-notes/issues/529
ActivityCompat.recreate(this);
break;
}
- case manage_account: {
- if (resultCode == RESULT_FIRST_USER) {
- selectAccount(null);
- }
- new Thread(() -> canMoveNoteToAnotherAccounts = db.getAccountsCount() > 1).start();
- break;
- }
default: {
try {
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> {
@@ -788,19 +631,20 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
try {
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null);
- db.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities);
- new Thread(() -> canMoveNoteToAnotherAccounts = db.getAccountsCount() > 1).start();
- Log.i(TAG, capabilities.toString());
- runOnUiThread(() -> selectAccount(ssoAccount.name));
- } catch (SQLiteException e) {
- // Happens when upgrading from version ≤ 1.0.1 and importing the account
- runOnUiThread(() -> selectAccount(ssoAccount.name));
+ LiveData<Account> createLiveData = mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities);
+ runOnUiThread(() -> createLiveData.observe(this, (account) -> {
+ new Thread(() -> {
+ Log.i(TAG, capabilities.toString());
+ final Account a = mainViewModel.getLocalAccountByAccountName(ssoAccount.name);
+ runOnUiThread(() -> mainViewModel.postCurrentAccount(a));
+ }).start();
+ }));
} catch (Exception e) {
// Happens when importing an already existing account the second time
- if (e instanceof TokenMismatchException && db.getLocalAccountByAccountName(ssoAccount.name) != null) {
+ if (e instanceof TokenMismatchException && mainViewModel.getLocalAccountByAccountName(ssoAccount.name) != null) {
Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported.");
runOnUiThread(() -> {
- selectAccount(ssoAccount.name);
+ mainViewModel.postCurrentAccount(mainViewModel.getLocalAccountByAccountName(ssoAccount.name));
// TODO there is already a sync in progress and results in displaying a TokenMissMatchException snackbar which conflicts with this one
coordinatorLayout.post(() -> BrandedSnackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG).show());
});
@@ -821,48 +665,11 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
}
}
- private void updateCurrentAccountAvatar() {
- try {
- String url = localAccount.getUrl();
- if (url != null) {
- Glide
- .with(this)
- .load(url + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64")
- .placeholder(R.drawable.ic_account_circle_grey_24dp)
- .error(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(activityBinding.launchAccountSwitcher);
- } else {
- Log.w(TAG, "url is null");
- }
- } catch (NullPointerException e) { // No local account - show generic header
- Glide
- .with(this)
- .load(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(activityBinding.launchAccountSwitcher);
- Log.w(TAG, "Tried to update username in drawer, but localAccount was null");
- }
- }
-
@Override
public void onNoteClick(int position, View v) {
- boolean hasCheckedItems = adapter.getSelected().size() > 0;
- if (hasCheckedItems) {
- if (!adapter.select(position)) {
- v.setSelected(false);
- adapter.deselect(position);
- } else {
- v.setSelected(true);
- }
- int size = adapter.getSelected().size();
- if (size > 0) {
- mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, size, size));
- } else {
- mActionMode.finish();
- }
- } else {
- DBNote note = (DBNote) adapter.getItem(position);
+ boolean hasCheckedItems = tracker.getSelection().size() > 0;
+ if (!hasCheckedItems) {
+ Note note = (Note) adapter.getItem(position);
Intent intent = new Intent(getApplicationContext(), EditNoteActivity.class);
intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
startActivityForResult(intent, show_single_note_cmd);
@@ -871,25 +678,8 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
@Override
public void onNoteFavoriteClick(int position, View view) {
- DBNote note = (DBNote) adapter.getItem(position);
- NotesDatabase db = NotesDatabase.getInstance(view.getContext());
- db.toggleFavorite(ssoAccount, note, syncCallBack);
- adapter.notifyItemChanged(position);
- refreshLists();
- }
-
- @Override
- public boolean onNoteLongClick(int position, View v) {
- boolean selected = adapter.select(position);
- if (selected) {
- v.setSelected(true);
- mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(
- this, this, db, localAccount.getId(), canMoveNoteToAnotherAccounts, adapter, listView, this::refreshLists, getSupportFragmentManager(), activityBinding.searchView
- ));
- int checkedItemCount = adapter.getSelected().size();
- mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, checkedItemCount, checkedItemCount));
- }
- return selected;
+ LiveData<Void> toggleLiveData = mainViewModel.toggleFavoriteAndSync(((Note) adapter.getItem(position)).getId());
+ toggleLiveData.observe(this, (next) -> toggleLiveData.removeObservers(this));
}
@Override
@@ -901,7 +691,6 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
}
}
- @SuppressLint("PrivateResource")
private void updateToolbars(boolean disableSearch) {
activityBinding.homeToolbar.setVisibility(disableSearch ? VISIBLE : GONE);
activityBinding.toolbar.setVisibility(disableSearch ? GONE : VISIBLE);
@@ -910,27 +699,6 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
if (disableSearch) {
activityBinding.searchView.setQuery(null, true);
}
- activityBinding.searchView.setIconified(disableSearch);
- }
-
- private void synchronize() {
- NoteServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
- if (!syncHelper.isSyncPossible()) {
- syncHelper.updateNetworkStatus();
- }
- if (syncHelper.isSyncPossible()) {
- runOnUiThread(() -> swipeRefreshLayout.setRefreshing(true));
- syncHelper.addCallbackPull(ssoAccount, syncCallBack);
- syncHelper.scheduleSync(ssoAccount, false);
- } else { // Sync is not possible
- runOnUiThread(() -> swipeRefreshLayout.setRefreshing(false));
- if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
- Log.d(TAG, "Network is connected, but sync is not possible");
- } else {
- Log.d(TAG, "Sync is not possible, because network is not connected");
- BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
- }
- }
}
@Override
@@ -939,55 +707,26 @@ public class MainActivity extends LockedActivity implements NoteClickListener, V
}
@Override
- public void onAccountChosen(LocalAccount localAccount) {
+ public void onAccountChosen(@NonNull Account localAccount) {
binding.drawerLayout.closeDrawer(GravityCompat.START);
- selectAccount(localAccount.getAccountName());
+ mainViewModel.postCurrentAccount(localAccount);
}
@Override
- public void onAccountDeleted(LocalAccount localAccount) {
- db.deleteAccount(localAccount);
- if (localAccount.getId() == this.localAccount.getId()) {
- List<LocalAccount> remainingAccounts = db.getAccounts();
- if (remainingAccounts.size() > 0) {
- this.localAccount = remainingAccounts.get(0);
- selectAccount(this.localAccount.getAccountName());
- } else {
- selectAccount(null);
- askForNewAccount(this);
- }
- }
- }
-
- @Override
- public void onAccountPicked(@NonNull LocalAccount account) {
- List<Integer> selection = new ArrayList<>(adapter.getSelected());
-
- adapter.deselect(0);
- for (Integer i : selection) {
- DBNote note = (DBNote) adapter.getItem(i);
- db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), db.getNote(note.getAccountId(), note.getId()), account.getId());
- RecyclerView.ViewHolder viewHolder = listView.findViewHolderForAdapterPosition(i);
- if (viewHolder != null) {
- viewHolder.itemView.setSelected(false);
- } else {
- Log.w(TAG, "Could not found viewholder to remove selection");
- }
+ public void onAccountPicked(@NonNull Account account) {
+ for (Long noteId : tracker.getSelection()) {
+ final LiveData<Note> moveLiveData = mainViewModel.moveNoteToAnotherAccount(account, noteId);
+ moveLiveData.observe(this, (v) -> {
+ tracker.deselect(noteId);
+ moveLiveData.removeObservers(this);
+ });
}
-
- mActionMode.finish();
- refreshLists();
}
@Override
public void onCategoryChosen(String category) {
- for (Integer i : new ArrayList<>(adapter.getSelected())) {
- DBNote note = (DBNote) adapter.getItem(i);
- note.setCategory(category);
- db.setCategory(ssoAccount, note, category, this::refreshLists);
- }
-
- mActionMode.finish();
- refreshLists();
+ final LiveData<Void> categoryLiveData = mainViewModel.setCategory(tracker.getSelection(), category);
+ categoryLiveData.observe(this, (next) -> categoryLiveData.removeObservers(this));
+ tracker.clearSelection();
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
new file mode 100644
index 00000000..0241951e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -0,0 +1,607 @@
+package it.niedermann.owncloud.notes.main;
+
+import android.accounts.NetworkErrorException;
+import android.app.Application;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.MainThread;
+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 androidx.lifecycle.SavedStateHandle;
+
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.exception.IntendedOfflineException;
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.NotesServerSyncHelper;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.Item;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.owncloud.notes.main.MainActivity.ADAPTER_KEY_RECENT;
+import static it.niedermann.owncloud.notes.main.MainActivity.ADAPTER_KEY_STARRED;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByCategory;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByInitials;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByTime;
+import static it.niedermann.owncloud.notes.shared.model.CategorySortingMethod.SORT_MODIFIED_DESC;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
+import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem;
+
+public class MainViewModel extends AndroidViewModel {
+
+ private static final String TAG = MainViewModel.class.getSimpleName();
+
+ private final SavedStateHandle state;
+
+ private static final String KEY_CURRENT_ACCOUNT = "currentAccount";
+ private static final String KEY_SEARCH_TERM = "searchTerm";
+ private static final String KEY_SELECTED_CATEGORY = "selectedCategory";
+ private static final String KEY_EXPANDED_CATEGORY = "expandedCategory";
+
+ @NonNull
+ private final NotesDatabase db;
+
+ @NonNull
+ private final MutableLiveData<Account> currentAccount = new MutableLiveData<>();
+ @NonNull
+ private final MutableLiveData<String> searchTerm = new MutableLiveData<>(null);
+ @NonNull
+ private final MutableLiveData<NavigationCategory> selectedCategory = new MutableLiveData<>(new NavigationCategory(RECENT));
+ @NonNull
+ private final MutableLiveData<String> expandedCategory = new MutableLiveData<>(null);
+
+ public MainViewModel(@NonNull Application application, @NonNull SavedStateHandle savedStateHandle) {
+ super(application);
+ this.db = NotesDatabase.getInstance(application);
+ this.state = savedStateHandle;
+ }
+
+ public void restoreInstanceState() {
+ Log.v(TAG, "[restoreInstanceState]");
+ final Account account = state.get(KEY_CURRENT_ACCOUNT);
+ if (account != null) {
+ postCurrentAccount(account);
+ }
+ postSearchTerm(state.get(KEY_SEARCH_TERM));
+ final NavigationCategory selectedCategory = state.get(KEY_SELECTED_CATEGORY);
+ if (selectedCategory != null) {
+ postSelectedCategory(selectedCategory);
+ Log.v(TAG, "[restoreInstanceState] - selectedCategory: " + selectedCategory);
+ }
+ postExpandedCategory(state.get(KEY_EXPANDED_CATEGORY));
+ }
+
+ @NonNull
+ public LiveData<Account> getCurrentAccount() {
+ return distinctUntilChanged(currentAccount);
+ }
+
+ public void postCurrentAccount(@NonNull Account account) {
+ state.set(KEY_CURRENT_ACCOUNT, account);
+ BrandingUtil.saveBrandColors(getApplication(), account.getColor(), account.getTextColor());
+ SingleAccountHelper.setCurrentAccount(getApplication(), account.getAccountName());
+
+ final Account currentAccount = this.currentAccount.getValue();
+ // If only ETag or colors change, we must not reset the navigation
+ // TODO in the long term we should store the last NavigationCategory for each Account
+ if (currentAccount == null || currentAccount.getId() != account.getId()) {
+ this.currentAccount.setValue(account);
+ this.searchTerm.setValue("");
+ this.selectedCategory.setValue(new NavigationCategory(RECENT));
+ }
+ }
+
+ @NonNull
+ public LiveData<String> getSearchTerm() {
+ return distinctUntilChanged(searchTerm);
+ }
+
+ public void postSearchTerm(String searchTerm) {
+ state.set(KEY_SEARCH_TERM, searchTerm);
+ this.searchTerm.postValue(searchTerm);
+ }
+
+ @NonNull
+ public LiveData<NavigationCategory> getSelectedCategory() {
+ return distinctUntilChanged(selectedCategory);
+ }
+
+ public void postSelectedCategory(@NonNull NavigationCategory selectedCategory) {
+ state.set(KEY_SELECTED_CATEGORY, selectedCategory);
+ Log.v(TAG, "[postSelectedCategory] - selectedCategory: " + selectedCategory);
+ this.selectedCategory.postValue(selectedCategory);
+
+ // Close sub categories
+ switch (selectedCategory.getType()) {
+ case RECENT:
+ case FAVORITES:
+ case UNCATEGORIZED: {
+ postExpandedCategory(null);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ String category = selectedCategory.getCategory();
+ if (category == null) {
+ postExpandedCategory(null);
+ Log.e(TAG, "navigation selection is a " + DEFAULT_CATEGORY + ", but the contained category is null.");
+ } else {
+ int slashIndex = category.indexOf('/');
+ String rootCategory = slashIndex < 0 ? category : category.substring(0, slashIndex);
+ String expandedCategory = getExpandedCategory().getValue();
+ if (expandedCategory != null && !expandedCategory.equals(rootCategory)) {
+ postExpandedCategory(null);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData<Pair<NavigationCategory, CategorySortingMethod>> getCategorySortingMethodOfSelectedCategory() {
+ return switchMap(getSelectedCategory(), selectedCategory -> map(db.getCategoryOrder(selectedCategory), sortingMethod -> new Pair<>(selectedCategory, sortingMethod)));
+ }
+
+ public LiveData<Void> modifyCategoryOrder(@NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[modifyCategoryOrder] - currentAccount: " + currentAccount.getAccountName());
+ db.modifyCategoryOrder(currentAccount.getId(), selectedCategory, sortingMethod);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public void postExpandedCategory(@Nullable String expandedCategory) {
+ state.set(KEY_EXPANDED_CATEGORY, expandedCategory);
+ this.expandedCategory.postValue(expandedCategory);
+ }
+
+ @NonNull
+ public LiveData<String> getExpandedCategory() {
+ return distinctUntilChanged(expandedCategory);
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData<List<Item>> getNotesListLiveData() {
+ final MutableLiveData<List<Item>> insufficientInformation = new MutableLiveData<>();
+ return distinctUntilChanged(switchMap(getCurrentAccount(), currentAccount -> {
+ Log.v(TAG, "[getNotesListLiveData] - currentAccount: " + currentAccount);
+ if (currentAccount == null) {
+ return insufficientInformation;
+ } else {
+ return switchMap(getSelectedCategory(), selectedCategory -> {
+ if (selectedCategory == null) {
+ return insufficientInformation;
+ } else {
+ Log.v(TAG, "[getNotesListLiveData] - selectedCategory: " + selectedCategory);
+ return switchMap(getSearchTerm(), searchTerm -> {
+ Log.v(TAG, "[getNotesListLiveData] - searchTerm: " + searchTerm);
+ return switchMap(getCategorySortingMethodOfSelectedCategory(), sortingMethod -> {
+ final long accountId = currentAccount.getId();
+ final String searchQueryOrWildcard = searchTerm == null ? "%" : "%" + searchTerm.trim() + "%";
+ Log.v(TAG, "[getNotesListLiveData] - sortMethod: " + sortingMethod.second);
+ final LiveData<List<Note>> fromDatabase;
+ switch (selectedCategory.getType()) {
+ case RECENT: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + RECENT);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? db.getNoteDao().searchRecentByModified$(accountId, searchQueryOrWildcard)
+ : db.getNoteDao().searchRecentLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case FAVORITES: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + FAVORITES);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? db.getNoteDao().searchFavoritesByModified$(accountId, searchQueryOrWildcard)
+ : db.getNoteDao().searchFavoritesLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case UNCATEGORIZED: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + UNCATEGORIZED);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? db.getNoteDao().searchUncategorizedByModified$(accountId, searchQueryOrWildcard)
+ : db.getNoteDao().searchUncategorizedLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category == null) {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ Log.v(TAG, "[getNotesListLiveData] - category: " + category);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? db.getNoteDao().searchCategoryByModified$(accountId, searchQueryOrWildcard, category)
+ : db.getNoteDao().searchCategoryLexicographically$(accountId, searchQueryOrWildcard, category);
+ break;
+ }
+ }
+
+ Log.v(TAG, "[getNotesListLiveData] - -------------------------------------");
+ return distinctUntilChanged(map(fromDatabase, noteList -> fromNotes(noteList, selectedCategory, sortingMethod.second)));
+ });
+ });
+ }
+ });
+ }
+ }));
+ }
+
+ private List<Item> fromNotes(List<Note> noteList, @NonNull NavigationCategory selectedCategory, @Nullable CategorySortingMethod sortingMethod) {
+ if (selectedCategory.getType() == DEFAULT_CATEGORY) {
+ final String category = selectedCategory.getCategory();
+ if (category != null) {
+ return fillListByCategory(noteList, category);
+ } else {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ }
+ if (sortingMethod == SORT_MODIFIED_DESC) {
+ return fillListByTime(getApplication(), noteList);
+ } else {
+ return fillListByInitials(getApplication(), noteList);
+ }
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData<List<NavigationItem>> getNavigationCategories() {
+ final MutableLiveData<List<NavigationItem>> insufficientInformation = new MutableLiveData<>();
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return insufficientInformation;
+ } else {
+ Log.v(TAG, "[getNavigationCategories] - currentAccount: " + currentAccount.getAccountName());
+ return switchMap(getExpandedCategory(), expandedCategory -> {
+ Log.v(TAG, "[getNavigationCategories] - expandedCategory: " + expandedCategory);
+ return switchMap(db.getNoteDao().count$(currentAccount.getId()), (count) -> {
+ Log.v(TAG, "[getNavigationCategories] - count: " + count);
+ return switchMap(db.getNoteDao().countFavorites$(currentAccount.getId()), (favoritesCount) -> {
+ Log.v(TAG, "[getNavigationCategories] - favoritesCount: " + favoritesCount);
+ return distinctUntilChanged(map(db.getNoteDao().getCategories$(currentAccount.getId()), fromDatabase ->
+ fromCategoriesWithNotesCount(getApplication(), expandedCategory, fromDatabase, count, favoritesCount)
+ ));
+ });
+ });
+ });
+ }
+ });
+ }
+
+ private static List<NavigationItem> fromCategoriesWithNotesCount(@NonNull Context context, @Nullable String expandedCategory, @NonNull List<CategoryWithNotesCount> fromDatabase, int count, int favoritesCount) {
+ final List<NavigationItem.CategoryNavigationItem> categories = convertToCategoryNavigationItem(context, fromDatabase);
+ final NavigationItem itemRecent = new NavigationItem(ADAPTER_KEY_RECENT, context.getString(R.string.label_all_notes), count, R.drawable.ic_access_time_grey600_24dp, RECENT);
+ final NavigationItem itemFavorites = new NavigationItem(ADAPTER_KEY_STARRED, context.getString(R.string.label_favorites), favoritesCount, R.drawable.ic_star_yellow_24dp, FAVORITES);
+
+ final ArrayList<NavigationItem> items = new ArrayList<>(fromDatabase.size() + 3);
+ items.add(itemRecent);
+ items.add(itemFavorites);
+ NavigationItem lastPrimaryCategory = null;
+ NavigationItem lastSecondaryCategory = null;
+ for (NavigationItem item : categories) {
+ int slashIndex = item.label.indexOf('/');
+ String currentPrimaryCategory = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
+ String currentSecondaryCategory = null;
+ boolean isCategoryOpen = currentPrimaryCategory.equals(expandedCategory);
+
+ if (isCategoryOpen && !currentPrimaryCategory.equals(item.label)) {
+ String currentCategorySuffix = item.label.substring(expandedCategory.length() + 1);
+ int subSlashIndex = currentCategorySuffix.indexOf('/');
+ currentSecondaryCategory = subSlashIndex < 0 ? currentCategorySuffix : currentCategorySuffix.substring(0, subSlashIndex);
+ }
+
+ boolean belongsToLastPrimaryCategory = lastPrimaryCategory != null && currentPrimaryCategory.equals(lastPrimaryCategory.label);
+ boolean belongsToLastSecondaryCategory = belongsToLastPrimaryCategory && lastSecondaryCategory != null && lastSecondaryCategory.label.equals(currentPrimaryCategory + "/" + currentSecondaryCategory);
+
+ if (isCategoryOpen && !belongsToLastPrimaryCategory && currentSecondaryCategory != null) {
+ lastPrimaryCategory = new NavigationItem("category:" + currentPrimaryCategory, currentPrimaryCategory, 0, NavigationAdapter.ICON_MULTIPLE_OPEN);
+ items.add(lastPrimaryCategory);
+ belongsToLastPrimaryCategory = true;
+ }
+
+ if (belongsToLastPrimaryCategory && belongsToLastSecondaryCategory) {
+ lastSecondaryCategory.count += item.count;
+ lastSecondaryCategory.icon = NavigationAdapter.ICON_SUB_MULTIPLE;
+ } else if (belongsToLastPrimaryCategory) {
+ if (isCategoryOpen) {
+ item.label = currentPrimaryCategory + "/" + currentSecondaryCategory;
+ item.id = "category:" + item.label;
+ item.icon = NavigationAdapter.ICON_SUB_FOLDER;
+ items.add(item);
+ lastSecondaryCategory = item;
+ } else {
+ lastPrimaryCategory.count += item.count;
+ lastPrimaryCategory.icon = NavigationAdapter.ICON_MULTIPLE;
+ lastSecondaryCategory = null;
+ }
+ } else {
+ if (isCategoryOpen) {
+ item.icon = NavigationAdapter.ICON_MULTIPLE_OPEN;
+ } else {
+ item.label = currentPrimaryCategory;
+ item.id = "category:" + item.label;
+ }
+ items.add(item);
+ lastPrimaryCategory = item;
+ lastSecondaryCategory = null;
+ }
+ }
+ return items;
+ }
+
+ public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
+ Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize capabilities for " + localAccount.getAccountName());
+ synchronizeCapabilities(localAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess() {
+ Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize notes for " + localAccount.getAccountName());
+ synchronizeNotes(localAccount, callback);
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ /**
+ * Updates the network status if necessary and pulls the latest {@link Capabilities} of the given {@param localAccount}
+ */
+ public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
+ new Thread(() -> {
+ final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
+ if (!syncHelper.isSyncPossible()) {
+ syncHelper.updateNetworkStatus();
+ }
+ if (syncHelper.isSyncPossible()) {
+ try {
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag());
+ db.getAccountDao().updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
+ db.getAccountDao().updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor());
+ localAccount.setColor(capabilities.getColor());
+ localAccount.setTextColor(capabilities.getTextColor());
+ BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor());
+ db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
+ callback.onSuccess();
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ db.getAccountDao().deleteAccount(localAccount);
+ callback.onError(e);
+ } catch (Exception e) {
+ if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+ Log.i(TAG, "[synchronizeCapabilities] Capabilities not modified.");
+ callback.onSuccess();
+ } else {
+ callback.onError(e);
+ }
+ }
+ } else {
+ if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
+ callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
+ } else {
+ callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
+ }
+ }
+ }, "SYNC_CAPABILITIES").start();
+ }
+
+ /**
+ * Updates the network status if necessary and pulls the latest notes of the given {@param localAccount}
+ */
+ public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback callback) {
+ new Thread(() -> {
+ Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
+ final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
+ if (!syncHelper.isSyncPossible()) {
+ syncHelper.updateNetworkStatus();
+ }
+ if (syncHelper.isSyncPossible()) {
+ syncHelper.scheduleSync(currentAccount, false);
+ callback.onSuccess();
+ } else { // Sync is not possible
+ if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
+ callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
+ } else {
+ callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
+ }
+ }
+ }, "SYNC_NOTES").start();
+ }
+
+ public LiveData<Boolean> getSyncStatus() {
+ return db.getNoteServerSyncHelper().getSyncStatus();
+ }
+
+ public LiveData<ArrayList<Throwable>> getSyncErrors() {
+ return db.getNoteServerSyncHelper().getSyncErrors();
+ }
+
+ public LiveData<Boolean> hasMultipleAccountsConfigured() {
+ return map(db.getAccountDao().countAccounts$(), (counter) -> counter != null && counter > 1);
+ }
+
+ @WorkerThread
+ public Account getLocalAccountByAccountName(String accountName) {
+ return db.getAccountDao().getAccountByName(accountName);
+ }
+
+ @WorkerThread
+ public List<Account> getAccounts() {
+ return db.getAccountDao().getAccounts();
+ }
+
+ public LiveData<Void> setCategory(Iterable<Long> noteIds, @NonNull String category) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[setCategory] - currentAccount: " + currentAccount.getAccountName());
+ for (Long noteId : noteIds) {
+ db.setCategory(currentAccount, noteId, category);
+ }
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData<Note> moveNoteToAnotherAccount(Account account, Long noteId) {
+ return switchMap(db.getNoteDao().getNoteById$(noteId), (note) -> {
+ Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + note);
+ return db.moveNoteToAnotherAccount(account, note);
+ });
+ }
+
+ public LiveData<Void> toggleFavoriteAndSync(long noteId) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[toggleFavoriteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ db.toggleFavoriteAndSync(currentAccount, noteId);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData<Void> deleteNoteAndSync(long id) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[deleteNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ db.deleteNoteAndSync(currentAccount, id);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData<Void> deleteNotesAndSync(@NonNull Collection<Long> ids) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[deleteNotesAndSync] - currentAccount: " + currentAccount.getAccountName());
+ for (Long id : ids) {
+ db.deleteNoteAndSync(currentAccount, id);
+ }
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
+ return db.addAccount(url, username, accountName, capabilities);
+ }
+
+ public LiveData<Note> getFullNote$(long id) {
+ return map(getFullNotesWithCategory(Collections.singleton(id)), input -> input.get(0));
+ }
+
+ @WorkerThread
+ public Note getFullNote(long id) {
+ return db.getNoteDao().getNoteById(id);
+ }
+
+ public LiveData<List<Note>> getFullNotesWithCategory(@NonNull Collection<Long> ids) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>();
+ } else {
+ Log.v(TAG, "[getNote] - currentAccount: " + currentAccount.getAccountName());
+ final MutableLiveData<List<Note>> notes = new MutableLiveData<>();
+ new Thread(() -> notes.postValue(
+ ids
+ .stream()
+ .map(id -> db.getNoteDao().getNoteById(id))
+ .collect(Collectors.toList())
+ )).start();
+ return notes;
+ }
+ });
+ }
+
+ public LiveData<Note> addNoteAndSync(Note note) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>();
+ } else {
+ Log.v(TAG, "[addNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ return db.addNoteAndSync(currentAccount, note);
+ }
+ });
+ }
+
+ public LiveData<Void> updateNoteAndSync(@NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount != null) {
+ Log.v(TAG, "[updateNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ db.updateNoteAndSync(currentAccount, oldNote, newContent, newTitle, null);
+ }
+ return new MutableLiveData<>(null);
+ });
+ }
+
+ public void createOrUpdateSingleNoteWidgetData(SingleNoteWidgetData data) {
+ db.getWidgetSingleNoteDao().createOrUpdateSingleNoteWidgetData(data);
+ }
+
+ public LiveData<Integer> getAccountsCount() {
+ return db.getAccountDao().countAccounts$();
+ }
+
+ @WorkerThread
+ public String collectNoteContents(@NonNull List<Long> noteIds) {
+ final StringBuilder noteContents = new StringBuilder();
+ for (Long noteId : noteIds) {
+ final Note fullNote = db.getNoteDao().getNoteById(noteId);
+ final String tempFullNote = fullNote.getContent();
+ if (!TextUtils.isEmpty(tempFullNote)) {
+ if (noteContents.length() > 0) {
+ noteContents.append("\n\n");
+ }
+ noteContents.append(tempFullNote);
+ }
+ }
+ return noteContents.toString();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
index 0ef5ea28..fa9bb879 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
@@ -2,68 +2,61 @@ package it.niedermann.owncloud.notes.main;
import android.content.Context;
import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.view.ActionMode.Callback;
-import androidx.appcompat.widget.SearchView;
-import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.selection.SelectionTracker;
import com.google.android.material.snackbar.Snackbar;
-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 java.util.ArrayList;
import java.util.List;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment;
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
-import it.niedermann.owncloud.notes.main.items.ItemAdapter;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
public class MultiSelectedActionModeCallback implements Callback {
@ColorInt
- private int colorAccent;
-
+ private final int colorAccent;
+ @NonNull
private final Context context;
- private final ViewProvider viewProvider;
- private final NotesDatabase db;
- private final long currentLocalAccountId;
+ @NonNull
+ private final View view;
+ @NonNull
+ private final MainViewModel mainViewModel;
+ @NonNull
+ private final LifecycleOwner lifecycleOwner;
private final boolean canMoveNoteToAnotherAccounts;
- private final ItemAdapter adapter;
- private final RecyclerView recyclerView;
- private final Runnable refreshLists;
+ @NonNull
+ private final SelectionTracker<Long> tracker;
+ @NonNull
private final FragmentManager fragmentManager;
- private final SearchView searchView;
public MultiSelectedActionModeCallback(
- Context context, ViewProvider viewProvider, NotesDatabase db, long currentLocalAccountId, boolean canMoveNoteToAnotherAccounts, ItemAdapter adapter, RecyclerView recyclerView, Runnable refreshLists, FragmentManager fragmentManager, SearchView searchView) {
+ @NonNull Context context, @NonNull View view, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, boolean canMoveNoteToAnotherAccounts, @NonNull SelectionTracker<Long> tracker, @NonNull FragmentManager fragmentManager) {
this.context = context;
- this.viewProvider = viewProvider;
- this.db = db;
- this.currentLocalAccountId = currentLocalAccountId;
+ this.view = view;
+ this.mainViewModel = mainViewModel;
+ this.lifecycleOwner = lifecycleOwner;
this.canMoveNoteToAnotherAccounts = canMoveNoteToAnotherAccounts;
- this.adapter = adapter;
- this.recyclerView = recyclerView;
- this.refreshLists = refreshLists;
+ this.tracker = tracker;
this.fragmentManager = fragmentManager;
- this.searchView = searchView;
final TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true);
@@ -98,76 +91,83 @@ public class MultiSelectedActionModeCallback implements Callback {
*/
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_delete:
- try {
- SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
- List<DBNote> deletedNotes = new ArrayList<>();
- List<Integer> selection = adapter.getSelected();
- for (Integer i : selection) {
- DBNote note = (DBNote) adapter.getItem(i);
- deletedNotes.add(db.getNote(note.getAccountId(), note.getId()));
- db.deleteNoteAndSync(ssoAccount, note.getId());
- }
- mode.finish(); // Action picked, so close the CAB
- //after delete selection has to be cleared
- searchView.setIconified(true);
- refreshLists.run();
- String deletedSnackbarTitle = deletedNotes.size() == 1
- ? context.getString(R.string.action_note_deleted, deletedNotes.get(0).getTitle())
- : context.getResources().getQuantityString(R.plurals.bulk_notes_deleted, deletedNotes.size(), deletedNotes.size());
- BrandedSnackbar.make(viewProvider.getView(), deletedSnackbarTitle, Snackbar.LENGTH_LONG)
- .setAction(R.string.action_undo, (View v) -> {
- db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run);
- for (DBNote deletedNote : deletedNotes) {
- db.addNoteAndSync(ssoAccount, deletedNote.getAccountId(), deletedNote);
- }
- refreshLists.run();
- String restoreSnackbarTitle = deletedNotes.size() == 1
- ? context.getString(R.string.action_note_restored, deletedNotes.get(0).getTitle())
- : context.getResources().getQuantityString(R.plurals.bulk_notes_restored, deletedNotes.size(), deletedNotes.size());
- BrandedSnackbar.make(viewProvider.getView(), restoreSnackbarTitle, Snackbar.LENGTH_SHORT)
- .show();
- })
- .show();
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- e.printStackTrace();
- }
- return true;
- case R.id.menu_move:
- AccountPickerDialogFragment
- .newInstance(currentLocalAccountId)
- .show(fragmentManager, MainActivity.class.getSimpleName());
- return true;
- case R.id.menu_share:
- final String subject = (adapter.getSelected().size() == 1)
- ? ((DBNote) adapter.getItem(adapter.getSelected().get(0))).getTitle()
- : context.getResources().getQuantityString(R.plurals.share_multiple, adapter.getSelected().size(), adapter.getSelected().size());
- final StringBuilder noteContents = new StringBuilder();
- for (Integer i : adapter.getSelected()) {
- final DBNote noteWithoutContent = (DBNote) adapter.getItem(i);
- final String tempFullNote = db.getNote(noteWithoutContent.getAccountId(), noteWithoutContent.getId()).getContent();
- if (!TextUtils.isEmpty(tempFullNote)) {
- if (noteContents.length() > 0) {
- noteContents.append("\n\n");
- }
- noteContents.append(tempFullNote);
- }
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_delete) {
+ final List<Long> selection = new ArrayList<>(tracker.getSelection().size());
+ for (Long sel : tracker.getSelection()) {
+ selection.add(sel);
+ }
+ final LiveData<List<Note>> fullNotes$ = mainViewModel.getFullNotesWithCategory(selection);
+ fullNotes$.observe(lifecycleOwner, (fullNotes) -> {
+ fullNotes$.removeObservers(lifecycleOwner);
+ tracker.clearSelection();
+ final LiveData<Void> deleteLiveData = mainViewModel.deleteNotesAndSync(selection);
+ deleteLiveData.observe(lifecycleOwner, (next) -> deleteLiveData.removeObservers(lifecycleOwner));
+ String deletedSnackbarTitle = fullNotes.size() == 1
+ ? context.getString(R.string.action_note_deleted, fullNotes.get(0).getTitle())
+ : context.getResources().getQuantityString(R.plurals.bulk_notes_deleted, fullNotes.size(), fullNotes.size());
+ BrandedSnackbar.make(view, deletedSnackbarTitle, Snackbar.LENGTH_LONG)
+ .setAction(R.string.action_undo, (View v) -> {
+ for (Note deletedNote : fullNotes) {
+ final LiveData<Note> undoLiveData = mainViewModel.addNoteAndSync(deletedNote);
+ undoLiveData.observe(lifecycleOwner, (o) -> undoLiveData.removeObservers(lifecycleOwner));
+ }
+ String restoreSnackbarTitle = fullNotes.size() == 1
+ ? context.getString(R.string.action_note_restored, fullNotes.get(0).getTitle())
+ : context.getResources().getQuantityString(R.plurals.bulk_notes_restored, fullNotes.size(), fullNotes.size());
+ BrandedSnackbar.make(view, restoreSnackbarTitle, Snackbar.LENGTH_SHORT)
+ .show();
+ })
+ .show();
+ });
+ return true;
+ } else if (itemId == R.id.menu_move) {
+ final LiveData<Account> currentAccount$ = mainViewModel.getCurrentAccount();
+ currentAccount$.observe(lifecycleOwner, account -> {
+ currentAccount$.removeObservers(lifecycleOwner);
+ new Thread(() -> {
+ AccountPickerDialogFragment
+ .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId())
+ .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName());
+ }).start();
+ });
+ return true;
+ } else if (itemId == R.id.menu_share) {
+ final List<Long> selection = new ArrayList<>(tracker.getSelection().size());
+ for (Long sel : tracker.getSelection()) {
+ selection.add(sel);
+ }
+ tracker.clearSelection();
+
+ new Thread(() -> {
+ if (selection.size() == 1) {
+ final Note note = mainViewModel.getFullNote(selection.get(0));
+ ShareUtil.openShareDialog(context, note.getTitle(), note.getContent());
+ } else {
+ ShareUtil.openShareDialog(context,
+ context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
+ mainViewModel.collectNoteContents(selection));
}
- ShareUtil.openShareDialog(context, subject, noteContents.toString());
- return true;
- case R.id.menu_category:
+ }).start();
+ return true;
+ } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it
+ final LiveData<Account> accountLiveData = mainViewModel.getCurrentAccount();
+ accountLiveData.observe(lifecycleOwner, account -> {
+ accountLiveData.removeObservers(lifecycleOwner);
CategoryDialogFragment
- .newInstance(((DBNote) adapter.getItem(adapter.getSelected().get(0))).getAccountId(), "")
+ .newInstance(account.getId(), "")
.show(fragmentManager, CategoryDialogFragment.class.getSimpleName());
- default:
- return false;
+ });
+ return true;
}
+ return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
- adapter.clearSelection(recyclerView);
- adapter.notifyDataSetChanged();
+ if (mode != null) {
+ mode.finish();
+ }
+ tracker.clearSelection();
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java
deleted file mode 100644
index d12a92e7..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package it.niedermann.owncloud.notes.main;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.branding.BrandingUtil;
-import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
-import it.niedermann.owncloud.notes.shared.util.NoteUtil;
-
-public class NavigationAdapter extends RecyclerView.Adapter<NavigationAdapter.ViewHolder> {
-
- @NonNull
- private final Context context;
- @ColorInt
- private int mainColor;
- @DrawableRes
- public static final int ICON_FOLDER = R.drawable.ic_folder_grey600_24dp;
- @DrawableRes
- public static final int ICON_NOFOLDER = R.drawable.ic_folder_open_grey600_24dp;
- @DrawableRes
- public static final int ICON_SUB_FOLDER = R.drawable.ic_folder_grey600_18dp;
- @DrawableRes
- public static final int ICON_MULTIPLE = R.drawable.ic_create_new_folder_grey600_24dp;
- @DrawableRes
- public static final int ICON_MULTIPLE_OPEN = R.drawable.ic_folder_grey600_24dp;
- @DrawableRes
- public static final int ICON_SUB_MULTIPLE = R.drawable.ic_create_new_folder_grey600_18dp;
-
- public void applyBrand(int mainColor, int textColor) {
- this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor);
- notifyDataSetChanged();
- }
-
- public static class NavigationItem {
- @NonNull
- public String id;
- @NonNull
- public String label;
- @DrawableRes
- public int icon;
- @Nullable
- public Integer count;
-
- public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon) {
- this.id = id;
- this.label = label;
- this.count = count;
- this.icon = icon;
- }
- }
-
- public static class CategoryNavigationItem extends NavigationItem {
- @NonNull
- public Long categoryId;
-
- public CategoryNavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon, @NonNull Long categoryId) {
- super(id, label, count, icon);
- this.categoryId = categoryId;
- }
- }
-
- class ViewHolder extends RecyclerView.ViewHolder {
- @NonNull
- private final View view;
-
- @NonNull
- private final TextView name;
- @NonNull
- private final TextView count;
- @NonNull
- private final ImageView icon;
-
- private NavigationItem currentItem;
-
- ViewHolder(@NonNull View itemView, @NonNull final ClickListener clickListener) {
- super(itemView);
- view = itemView;
- ItemNavigationBinding binding = ItemNavigationBinding.bind(view);
- this.name = binding.navigationItemLabel;
- this.count = binding.navigationItemCount;
- this.icon = binding.navigationItemIcon;
- icon.setOnClickListener(view -> clickListener.onIconClick(currentItem));
- itemView.setOnClickListener(view -> clickListener.onItemClick(currentItem));
- }
-
- private void bind(@NonNull NavigationItem item) {
- currentItem = item;
- boolean isSelected = item.id.equals(selectedItem);
- name.setText(NoteUtil.extendCategory(item.label));
- count.setVisibility(item.count == null ? View.GONE : View.VISIBLE);
- count.setText(String.valueOf(item.count));
- if (item.icon > 0) {
- icon.setImageDrawable(DrawableCompat.wrap(icon.getResources().getDrawable(item.icon)));
- icon.setVisibility(View.VISIBLE);
- } else {
- icon.setVisibility(View.GONE);
- }
- int textColor = isSelected ? mainColor : view.getResources().getColor(R.color.fg_default);
-
- name.setTextColor(textColor);
- count.setTextColor(textColor);
- icon.setColorFilter(isSelected ? textColor : 0);
-
- view.setSelected(isSelected);
- }
- }
-
- public interface ClickListener {
- void onItemClick(NavigationItem item);
-
- void onIconClick(NavigationItem item);
- }
-
- @NonNull
- private List<NavigationItem> items = new ArrayList<>();
- private String selectedItem = null;
- @NonNull
- private final ClickListener clickListener;
-
- public NavigationAdapter(@NonNull Context context, @NonNull ClickListener clickListener) {
- this.context = context;
- this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context));
- this.clickListener = clickListener;
- }
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false);
- return new ViewHolder(v, clickListener);
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- holder.bind(items.get(position));
- }
-
- @Override
- public int getItemCount() {
- return items.size();
- }
-
- public void setItems(@NonNull List<NavigationItem> items) {
- this.items = items;
- notifyDataSetChanged();
- }
-
- public void setSelectedItem(String id) {
- selectedItem = id;
- notifyDataSetChanged();
- }
-
- public String getSelectedItem() {
- return selectedItem;
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java
index da5425b7..5addabfb 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java
@@ -4,16 +4,17 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.text.TextUtils;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
+import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
@@ -26,15 +27,15 @@ import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTit
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding;
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding;
import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.Item;
-import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolder;
import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolderOnlyTitle;
import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithExcerpt;
import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithoutExcerpt;
import it.niedermann.owncloud.notes.main.items.section.SectionItem;
import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.Item;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
@@ -49,10 +50,11 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
private final NoteClickListener noteClickListener;
private final boolean gridView;
- private List<Item> itemList = new ArrayList<>();
+ @NonNull
+ private final List<Item> itemList = new ArrayList<>();
private boolean showCategory = true;
private CharSequence searchQuery;
- private final List<Integer> selected = new ArrayList<>();
+ private SelectionTracker<Long> tracker = null;
@Px
private final float fontSize;
private final boolean monospace;
@@ -60,6 +62,8 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
private int mainColor;
@ColorInt
private int textColor;
+ @Nullable
+ private Integer swipedPosition;
public <T extends Context & NoteClickListener> ItemAdapter(@NonNull T context, boolean gridView) {
this.noteClickListener = context;
@@ -69,20 +73,17 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
this.fontSize = getFontSizeFromPreferences(context, sp);
this.monospace = sp.getBoolean(context.getString(R.string.pref_key_font), false);
- // FIXME see getItemId()
- // setHasStableIds(true);
+ setHasStableIds(true);
}
- /*
- FIXME this causes {@link it.niedermann.owncloud.notes.noteslist.items.list.NotesListViewItemTouchHelper} to not call clearView anymore → After marking a note as favorite, it stays yellow.
- @Override
- public long getItemId(int position) {
- return getItemViewType(position) == TYPE_SECTION
- ? ((SectionItem) getItem(position)).getTitle().hashCode() * -1
- : ((DBNote) getItem(position)).getId();
- }
- */
+ // FIXME this causes {@link it.niedermann.owncloud.notes.noteslist.items.list.NotesListViewItemTouchHelper} to not call clearView anymore → After marking a note as favorite, it stays yellow.
+ @Override
+ public long getItemId(int position) {
+ return getItemViewType(position) == TYPE_SECTION
+ ? ((SectionItem) getItem(position)).getTitle().hashCode() * -1
+ : ((Note) getItem(position)).getId();
+ }
/**
* Updates the item list and notifies respective view to update.
@@ -90,43 +91,27 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
* @param itemList List of items to be set
*/
public void setItemList(@NonNull List<Item> itemList) {
- this.itemList = itemList;
- notifyDataSetChanged();
- }
-
- /**
- * Adds the given note to the top of the list.
- *
- * @param note Note that should be added.
- */
- public void add(@NonNull DBNote note) {
- itemList.add(0, note);
- notifyItemInserted(0);
- notifyItemChanged(0);
- }
-
- /**
- * Removes all items from the adapter.
- */
- public void removeAll() {
- itemList.clear();
+ this.itemList.clear();
+ this.itemList.addAll(itemList);
+ this.swipedPosition = null;
notifyDataSetChanged();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (gridView) {
switch (viewType) {
case TYPE_SECTION: {
- return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext())));
+ return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(inflater));
}
case TYPE_NOTE_ONLY_TITLE: {
- return new NoteViewGridHolderOnlyTitle(ItemNotesListNoteItemGridOnlyTitleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize);
+ return new NoteViewGridHolderOnlyTitle(ItemNotesListNoteItemGridOnlyTitleBinding.inflate(inflater, parent, false), noteClickListener, monospace, fontSize);
}
case TYPE_NOTE_WITH_EXCERPT:
case TYPE_NOTE_WITHOUT_EXCERPT: {
- return new NoteViewGridHolder(ItemNotesListNoteItemGridBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize);
+ return new NoteViewGridHolder(ItemNotesListNoteItemGridBinding.inflate(inflater, parent, false), noteClickListener, monospace, fontSize);
}
default: {
throw new IllegalArgumentException("Not supported viewType: " + viewType);
@@ -135,14 +120,14 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
} else {
switch (viewType) {
case TYPE_SECTION: {
- return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext())));
+ return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(inflater));
}
case TYPE_NOTE_WITH_EXCERPT: {
- return new NoteViewHolderWithExcerpt(ItemNotesListNoteItemWithExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener);
+ return new NoteViewHolderWithExcerpt(ItemNotesListNoteItemWithExcerptBinding.inflate(inflater, parent, false), noteClickListener);
}
case TYPE_NOTE_ONLY_TITLE:
case TYPE_NOTE_WITHOUT_EXCERPT: {
- return new NoteViewHolderWithoutExcerpt(ItemNotesListNoteItemWithoutExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener);
+ return new NoteViewHolderWithoutExcerpt(ItemNotesListNoteItemWithoutExcerptBinding.inflate(inflater, parent, false), noteClickListener);
}
default: {
throw new IllegalArgumentException("Not supported viewType: " + viewType);
@@ -153,6 +138,16 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
+ boolean isSelected = false;
+ if (tracker != null) {
+ Long itemId = getItemId(position);
+ if (tracker.isSelected(itemId)) {
+ tracker.select(itemId);
+ isSelected = true;
+ } else {
+ tracker.deselect(itemId);
+ }
+ }
switch (getItemViewType(position)) {
case TYPE_SECTION: {
((SectionViewHolder) holder).bind((SectionItem) itemList.get(position));
@@ -161,42 +156,14 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
case TYPE_NOTE_WITH_EXCERPT:
case TYPE_NOTE_WITHOUT_EXCERPT:
case TYPE_NOTE_ONLY_TITLE: {
- ((NoteViewHolder) holder).bind((DBNote) itemList.get(position), showCategory, mainColor, textColor, searchQuery);
+ ((NoteViewHolder) holder).bind(isSelected, (Note) itemList.get(position), showCategory, mainColor, textColor, searchQuery);
break;
}
}
}
- public boolean select(Integer position) {
- return !selected.contains(position) && selected.add(position);
- }
-
- public void clearSelection(@NonNull RecyclerView recyclerView) {
- for (Integer i : getSelected()) {
- RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i);
- if (viewHolder != null) {
- viewHolder.itemView.setSelected(false);
- } else {
- Log.w(TAG, "Could not found viewholder to remove selection");
- }
- }
- selected.clear();
- }
-
- @NonNull
- public List<Integer> getSelected() {
- return selected;
- }
-
- public void deselect(Integer position) {
- for (int i = 0; i < selected.size(); i++) {
- if (selected.get(i).equals(position)) {
- //position was selected and removed
- selected.remove(i);
- return;
- }
- }
- // position was not selected
+ public void setTracker(SelectionTracker<Long> tracker) {
+ this.tracker = tracker;
}
public Item getItem(int notePosition) {
@@ -225,7 +192,7 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
throw new IllegalArgumentException("Item at position " + position + " must not be null");
}
if (getItem(position).isSection()) return TYPE_SECTION;
- DBNote note = (DBNote) getItem(position);
+ Note note = (Note) getItem(position);
if (TextUtils.isEmpty(note.getExcerpt())) {
if (TextUtils.isEmpty(note.getCategory())) {
return TYPE_NOTE_ONLY_TITLE;
@@ -245,10 +212,11 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
public void setHighlightSearchQuery(CharSequence searchQuery) {
this.searchQuery = searchQuery;
+ notifyDataSetChanged();
}
/**
- * @return the position of the first item which matches the given viewtype, -1 if not available
+ * @return the position of the first {@link Item} which matches the given viewtype, -1 if not available
*/
public int getFirstPositionOfViewType(@IntRange(from = 0, to = 3) int viewType) {
for (int i = 0; i < itemList.size(); i++) {
@@ -258,4 +226,13 @@ public class ItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> i
}
return -1;
}
+
+ @Nullable
+ public Integer getSwipedPosition() {
+ return swipedPosition;
+ }
+
+ public void setSwipedPosition(@Nullable Integer swipedPosition) {
+ this.swipedPosition = swipedPosition;
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java
index a5a25331..131609fa 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java
@@ -3,8 +3,6 @@ package it.niedermann.owncloud.notes.main.items;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
-import android.graphics.drawable.GradientDrawable;
-import android.os.Build;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
@@ -20,6 +18,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.recyclerview.selection.ItemDetailsLookup;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.chip.Chip;
@@ -31,7 +30,7 @@ import it.niedermann.android.util.ColorUtil;
import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
@@ -50,9 +49,9 @@ public abstract class NoteViewHolder extends RecyclerView.ViewHolder {
}
@CallSuper
- public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ itemView.setSelected(isSelected);
itemView.setOnClickListener((view) -> noteClickListener.onNoteClick(getLayoutPosition(), view));
- itemView.setOnLongClickListener((view) -> noteClickListener.onNoteLongClick(getLayoutPosition(), view));
}
protected void bindStatus(AppCompatImageView noteStatus, DBStatus status, int mainColor) {
@@ -65,8 +64,8 @@ public abstract class NoteViewHolder extends RecyclerView.ViewHolder {
noteCategory.setVisibility(showCategory && !category.isEmpty() ? View.VISIBLE : View.GONE);
noteCategory.setText(category);
- @ColorInt int categoryForeground;
- @ColorInt int categoryBackground;
+ @ColorInt final int categoryForeground;
+ @ColorInt final int categoryBackground;
if (isDarkThemeActive) {
if (ColorUtil.INSTANCE.isColorDark(mainColor)) {
@@ -92,8 +91,13 @@ public abstract class NoteViewHolder extends RecyclerView.ViewHolder {
noteCategory.setTextColor(categoryForeground);
if (noteCategory instanceof Chip) {
- ((Chip) noteCategory).setChipStrokeColor(ColorStateList.valueOf(categoryBackground));
- ((Chip) noteCategory).setChipBackgroundColor(ColorStateList.valueOf(isDarkThemeActive ? categoryBackground : Color.TRANSPARENT));
+ final Chip chip = (Chip) noteCategory;
+ chip.setChipStrokeColor(ColorStateList.valueOf(categoryBackground));
+ if(isDarkThemeActive) {
+ chip.setChipBackgroundColor(ColorStateList.valueOf(categoryBackground));
+ } else {
+ chip.setChipBackgroundColorResource(R.color.grid_item_background_selector);
+ }
} else {
DrawableCompat.setTint(noteCategory.getBackground(), categoryBackground);
}
@@ -131,4 +135,18 @@ public abstract class NoteViewHolder extends RecyclerView.ViewHolder {
@Nullable
public abstract View getNoteSwipeable();
+
+ public ItemDetailsLookup.ItemDetails<Long> getItemDetails() {
+ return new ItemDetailsLookup.ItemDetails<Long>() {
+ @Override
+ public int getPosition() {
+ return getAdapterPosition();
+ }
+
+ @Override
+ public Long getSelectionKey() {
+ return getItemId();
+ }
+ };
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
index 765b970a..7c4cecfe 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
@@ -11,9 +11,9 @@ import androidx.annotation.Nullable;
import androidx.annotation.Px;
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@@ -39,12 +39,12 @@ public class NoteViewGridHolder extends NoteViewHolder {
throw new UnsupportedOperationException(NoteViewGridHolder.class.getSimpleName() + " does not support swiping");
}
- public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
- super.bind(note, showCategory, mainColor, textColor, searchQuery);
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
@NonNull final Context context = itemView.getContext();
bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
bindStatus(binding.noteStatus, note.getStatus(), mainColor);
- bindFavorite(binding.noteFavorite, note.isFavorite());
+ bindFavorite(binding.noteFavorite, note.getFavorite());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), mainColor);
binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
index 6468e964..416d99d2 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
@@ -10,9 +10,9 @@ import androidx.annotation.Nullable;
import androidx.annotation.Px;
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
public class NoteViewGridHolderOnlyTitle extends NoteViewHolder {
@NonNull
@@ -32,11 +32,11 @@ public class NoteViewGridHolderOnlyTitle extends NoteViewHolder {
throw new UnsupportedOperationException(NoteViewGridHolderOnlyTitle.class.getSimpleName() + " does not support swiping");
}
- public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
- super.bind(note, showCategory, mainColor, textColor, searchQuery);
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
@NonNull final Context context = itemView.getContext();
bindStatus(binding.noteStatus, note.getStatus(), mainColor);
- bindFavorite(binding.noteFavorite, note.isFavorite());
+ bindFavorite(binding.noteFavorite, note.getFavorite());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java
index 0f41d745..8d1cfe07 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java
@@ -8,10 +8,10 @@ import androidx.annotation.Nullable;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
-import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
public class NoteViewHolderWithExcerpt extends NoteViewHolder {
@NonNull
@@ -28,13 +28,13 @@ public class NoteViewHolderWithExcerpt extends NoteViewHolder {
binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention);
}
- public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
- super.bind(note, showCategory, mainColor, textColor, searchQuery);
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
@NonNull final Context context = itemView.getContext();
binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f);
bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
bindStatus(binding.noteStatus, note.getStatus(), mainColor);
- bindFavorite(binding.noteFavorite, note.isFavorite());
+ bindFavorite(binding.noteFavorite, note.getFavorite());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), mainColor);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java
index 9b056925..3ca136e4 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java
@@ -8,10 +8,10 @@ import androidx.annotation.Nullable;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
-import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
public class NoteViewHolderWithoutExcerpt extends NoteViewHolder {
@NonNull
@@ -28,13 +28,13 @@ public class NoteViewHolderWithoutExcerpt extends NoteViewHolder {
binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention);
}
- public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
- super.bind(note, showCategory, mainColor, textColor, searchQuery);
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
@NonNull final Context context = itemView.getContext();
binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f);
bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
bindStatus(binding.noteStatus, note.getStatus(), mainColor);
- bindFavorite(binding.noteFavorite, note.isFavorite());
+ bindFavorite(binding.noteFavorite, note.getFavorite());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java
index ed4b28dc..5730aecf 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java
@@ -1,29 +1,29 @@
package it.niedermann.owncloud.notes.main.items.list;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.util.Log;
import android.view.View;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.main.MainViewModel;
import it.niedermann.owncloud.notes.main.items.ItemAdapter;
import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
public class NotesListViewItemTouchHelper extends ItemTouchHelper {
@@ -31,14 +31,13 @@ public class NotesListViewItemTouchHelper extends ItemTouchHelper {
private static final int UNDO_DURATION = 12_000;
public NotesListViewItemTouchHelper(
- @NonNull SingleSignOnAccount ssoAccount,
@NonNull Context context,
- @NonNull NotesDatabase db,
+ @NonNull MainViewModel mainViewModel,
+ @NonNull LifecycleOwner lifecycleOwner,
+ @NonNull SelectionTracker<Long> tracker,
@NonNull ItemAdapter adapter,
- @NonNull ISyncCallback syncCallBack,
- @NonNull Runnable refreshLists,
- @Nullable SwipeRefreshLayout swipeRefreshLayout,
- @Nullable ViewProvider viewProvider,
+ @NonNull SwipeRefreshLayout swipeRefreshLayout,
+ @NonNull View view,
boolean gridView) {
super(new SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
private boolean swipeRefreshLayoutEnabled;
@@ -67,34 +66,35 @@ public class NotesListViewItemTouchHelper extends ItemTouchHelper {
* @param viewHolder RecyclerView.ViewHoler
* @param direction int
*/
+ @SuppressLint("WrongConstant")
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
switch (direction) {
case ItemTouchHelper.LEFT:
- final DBNote dbNoteWithoutContent = (DBNote) adapter.getItem(viewHolder.getLayoutPosition());
- final DBNote dbNote = db.getNote(dbNoteWithoutContent.getAccountId(), dbNoteWithoutContent.getId());
- db.deleteNoteAndSync(ssoAccount, dbNote.getId());
- adapter.remove(dbNote);
- refreshLists.run();
- Log.v(TAG, "Item deleted through swipe ----------------------------------------------");
- if (viewProvider == null) {
- Toast.makeText(context, context.getString(R.string.action_note_deleted, dbNote.getTitle()), Toast.LENGTH_LONG).show();
- } else {
- BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_deleted, dbNote.getTitle()), UNDO_DURATION)
+ viewHolder.setIsRecyclable(false);
+ final Note dbNoteWithoutContent = (Note) adapter.getItem(viewHolder.getLayoutPosition());
+ final LiveData<Note> dbNoteLiveData = mainViewModel.getFullNote$(dbNoteWithoutContent.getId());
+ dbNoteLiveData.observe(lifecycleOwner, (dbNote) -> {
+ dbNoteLiveData.removeObservers(lifecycleOwner);
+ tracker.deselect(dbNote.getId());
+ final LiveData<Void> deleteLiveData = mainViewModel.deleteNoteAndSync(dbNote.getId());
+ deleteLiveData.observe(lifecycleOwner, (next) -> deleteLiveData.removeObservers(lifecycleOwner));
+ Log.v(TAG, "Item deleted through swipe ----------------------------------------------");
+ BrandedSnackbar.make(view, context.getString(R.string.action_note_deleted, dbNote.getTitle()), UNDO_DURATION)
.setAction(R.string.action_undo, (View v) -> {
- db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run);
- db.addNoteAndSync(ssoAccount, dbNote.getAccountId(), dbNote);
- refreshLists.run();
- BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_restored, dbNote.getTitle()), Snackbar.LENGTH_SHORT)
+ final LiveData<Note> undoLiveData = mainViewModel.addNoteAndSync(dbNote);
+ undoLiveData.observe(lifecycleOwner, (o) -> undoLiveData.removeObservers(lifecycleOwner));
+ BrandedSnackbar.make(view, context.getString(R.string.action_note_restored, dbNote.getTitle()), Snackbar.LENGTH_SHORT)
.show();
})
.show();
- }
+ });
break;
case ItemTouchHelper.RIGHT:
- final DBNote adapterNote = (DBNote) adapter.getItem(viewHolder.getLayoutPosition());
- db.toggleFavorite(ssoAccount, adapterNote, syncCallBack);
- refreshLists.run();
+ viewHolder.setIsRecyclable(false);
+ final Note adapterNote = (Note) adapter.getItem(viewHolder.getLayoutPosition());
+ final LiveData<Void> toggleLiveData = mainViewModel.toggleFavoriteAndSync(adapterNote.getId());
+ toggleLiveData.observe(lifecycleOwner, (next) -> toggleLiveData.removeObservers(lifecycleOwner));
break;
default:
//NoOp
@@ -103,7 +103,7 @@ public class NotesListViewItemTouchHelper extends ItemTouchHelper {
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
- NoteViewHolder noteViewHolder = (NoteViewHolder) viewHolder;
+ final NoteViewHolder noteViewHolder = (NoteViewHolder) viewHolder;
// show swipe icon on the side
noteViewHolder.showSwipe(dX > 0);
// move only swipeable part of item (not leave-behind)
@@ -112,10 +112,13 @@ public class NotesListViewItemTouchHelper extends ItemTouchHelper {
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
- if (actionState == ACTION_STATE_SWIPE && swipeRefreshLayout != null) {
+ if (actionState == ACTION_STATE_SWIPE) {
Log.i(TAG, "Start swiping, disable swipeRefreshLayout");
swipeRefreshLayoutEnabled = swipeRefreshLayout.isEnabled();
swipeRefreshLayout.setEnabled(false);
+ if (viewHolder != null) {
+ adapter.setSwipedPosition(viewHolder.getLayoutPosition());
+ }
}
super.onSelectedChanged(viewHolder, actionState);
}
@@ -123,10 +126,9 @@ public class NotesListViewItemTouchHelper extends ItemTouchHelper {
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
Log.i(TAG, "End swiping, resetting swipeRefreshLayout state");
- if (swipeRefreshLayout != null) {
- swipeRefreshLayout.setEnabled(swipeRefreshLayoutEnabled);
- }
+ swipeRefreshLayout.setEnabled(swipeRefreshLayoutEnabled);
getDefaultUIUtil().clearView(((NoteViewHolder) viewHolder).getNoteSwipeable());
+ adapter.setSwipedPosition(null);
}
@Override
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java
index bd01163d..6f7ca1c7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java
@@ -1,5 +1,7 @@
package it.niedermann.owncloud.notes.main.items.section;
+import androidx.annotation.NonNull;
+
import it.niedermann.owncloud.notes.shared.model.Item;
public class SectionItem implements Item {
@@ -22,4 +24,27 @@ public class SectionItem implements Item {
public boolean isSection() {
return true;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SectionItem)) return false;
+
+ SectionItem that = (SectionItem) o;
+
+ return title != null ? title.equals(that.title) : that.title == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return title != null ? title.hashCode() : 0;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "SectionItem{" +
+ "title='" + title + '\'' +
+ '}';
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java
new file mode 100644
index 00000000..bbed62f4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java
@@ -0,0 +1,33 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.widget.RecyclerView;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+public class ItemIdKeyProvider extends ItemKeyProvider<Long> {
+ private final RecyclerView recyclerView;
+
+ public ItemIdKeyProvider(RecyclerView recyclerView) {
+ super(SCOPE_MAPPED);
+ this.recyclerView = recyclerView;
+ }
+
+ @Nullable
+ @Override
+ public Long getKey(int position) {
+ final RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
+ if (adapter == null) {
+ throw new IllegalStateException("RecyclerView adapter is not set!");
+ }
+ return adapter.getItemId(position);
+ }
+
+ @Override
+ public int getPosition(@NonNull Long key) {
+ final RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForItemId(key);
+ return viewHolder == null ? NO_POSITION : viewHolder.getLayoutPosition();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java
new file mode 100644
index 00000000..05537db1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java
@@ -0,0 +1,37 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemDetailsLookup;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+
+public class ItemLookup extends ItemDetailsLookup<Long> {
+
+ @NonNull
+ private final RecyclerView recyclerView;
+
+ public ItemLookup(@NonNull RecyclerView recyclerView) {
+ this.recyclerView = recyclerView;
+ }
+
+ @Nullable
+ @Override
+ public ItemDetails<Long> getItemDetails(@NonNull MotionEvent e) {
+ final View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
+ if (view != null) {
+ final RecyclerView.ViewHolder viewHolder = recyclerView.getChildViewHolder(view);
+ if (viewHolder instanceof NoteViewHolder) {
+ return ((NoteViewHolder) recyclerView.getChildViewHolder(view))
+ .getItemDetails();
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java
new file mode 100644
index 00000000..c1a8d3a0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java
@@ -0,0 +1,43 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.SelectionTracker;
+import androidx.recyclerview.selection.StorageStrategy;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+
+public class ItemSelectionTracker {
+
+ private ItemSelectionTracker() {
+ // Use build() method
+ }
+
+ public static SelectionTracker<Long> build(@NonNull RecyclerView recyclerView, @NonNull ItemAdapter adapter) {
+ return new SelectionTracker.Builder<>(
+ ItemSelectionTracker.class.getSimpleName(),
+ recyclerView,
+ new ItemIdKeyProvider(recyclerView),
+ new ItemLookup(recyclerView),
+ StorageStrategy.createLongStorage()
+ ).withSelectionPredicate(
+ new SelectionTracker.SelectionPredicate<Long>() {
+ @Override
+ public boolean canSetStateForKey(@NonNull Long key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ @Nullable Integer swipedPosition = adapter.getSwipedPosition();
+ return !adapter.getItem(position).isSection() && (swipedPosition == null || swipedPosition != position);
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return true;
+ }
+ }).build();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
new file mode 100644
index 00000000..82d88d11
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
@@ -0,0 +1,73 @@
+package it.niedermann.owncloud.notes.main.menu;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.FormattingHelpActivity;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.about.AboutActivity;
+import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.preferences.PreferencesActivity;
+
+public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {
+
+ public static final int SERVER_SETTINGS = 2;
+
+ @NonNull
+ private final MenuItem[] menuItems;
+ @NonNull
+ private final Consumer<MenuItem> onClick;
+ @NonNull
+ private final Context context;
+
+ public MenuAdapter(@NonNull Context context, @NonNull Account account, @NonNull Consumer<MenuItem> onClick) {
+ this.context = context;
+ this.menuItems = new MenuItem[]{
+ new MenuItem(new Intent(context, FormattingHelpActivity.class), R.string.action_formatting_help, R.drawable.ic_baseline_help_outline_24),
+ new MenuItem(generateTrashbinIntent(account), R.string.action_trashbin, R.drawable.ic_delete_grey600_24dp),
+ new MenuItem(new Intent(context, PreferencesActivity.class), SERVER_SETTINGS, R.string.action_settings, R.drawable.ic_settings_grey600_24dp),
+ new MenuItem(new Intent(context, AboutActivity.class), R.string.simple_about, R.drawable.ic_info_outline_grey600_24dp)
+ };
+ this.onClick = onClick;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @NonNull
+ @Override
+ public MenuViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new MenuViewHolder(ItemNavigationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull MenuViewHolder holder, int position) {
+ holder.bind(menuItems[position], onClick, ContextCompat.getColor(context, R.color.fg_default));
+ }
+
+ public void updateAccount(@NonNull Account account) {
+ menuItems[1].setIntent(new Intent(generateTrashbinIntent(account)));
+ }
+
+ @Override
+ public int getItemCount() {
+ return menuItems.length;
+ }
+
+ @NonNull
+ private static Intent generateTrashbinIntent(@NonNull Account account) {
+ return new Intent(Intent.ACTION_VIEW, Uri.parse(account.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin"));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuItem.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuItem.java
new file mode 100644
index 00000000..5503860c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuItem.java
@@ -0,0 +1,56 @@
+package it.niedermann.owncloud.notes.main.menu;
+
+import android.content.Intent;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+public class MenuItem {
+
+ @NonNull
+ private Intent intent;
+ @StringRes
+ private final int labelResource;
+ @DrawableRes
+ private final int drawableResource;
+
+ @Nullable
+ private Integer resultCode;
+
+ public MenuItem(@NonNull Intent intent, int labelResource, int drawableResource) {
+ this.intent = intent;
+ this.labelResource = labelResource;
+ this.drawableResource = drawableResource;
+ }
+
+ public MenuItem(@NonNull Intent intent, int resultCode, int labelResource, int drawableResource) {
+ this.intent = intent;
+ this.resultCode = resultCode;
+ this.labelResource = labelResource;
+ this.drawableResource = drawableResource;
+ }
+
+ @NonNull
+ public Intent getIntent() {
+ return intent;
+ }
+
+ public void setIntent(@NonNull Intent intent) {
+ this.intent = intent;
+ }
+
+ public int getLabelResource() {
+ return labelResource;
+ }
+
+ public int getDrawableResource() {
+ return drawableResource;
+ }
+
+ @Nullable
+ public Integer getResultCode() {
+ return resultCode;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java
new file mode 100644
index 00000000..0e08e279
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java
@@ -0,0 +1,32 @@
+package it.niedermann.owncloud.notes.main.menu;
+
+import android.content.Context;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
+
+import static android.view.View.GONE;
+
+public class MenuViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemNavigationBinding binding;
+
+ public MenuViewHolder(@NonNull ItemNavigationBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull MenuItem menuItem, @NonNull Consumer<MenuItem> onClick, @ColorInt int textColor) {
+ @NonNull Context context = itemView.getContext();
+ binding.navigationItemLabel.setText(context.getString(menuItem.getLabelResource()));
+ binding.navigationItemLabel.setTextColor(textColor);
+ binding.navigationItemIcon.setImageDrawable(ContextCompat.getDrawable(context, menuItem.getDrawableResource()));
+ binding.navigationItemCount.setVisibility(GONE);
+ binding.getRoot().setOnClickListener((v) -> onClick.accept(menuItem));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationAdapter.java
new file mode 100644
index 00000000..4e170901
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationAdapter.java
@@ -0,0 +1,94 @@
+package it.niedermann.owncloud.notes.main.navigation;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.main.MainActivity;
+
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
+
+public class NavigationAdapter extends RecyclerView.Adapter<NavigationViewHolder> {
+
+ @NonNull
+ private final Context context;
+ @ColorInt
+ private int mainColor;
+ @DrawableRes
+ public static final int ICON_FOLDER = R.drawable.ic_folder_grey600_24dp;
+ @DrawableRes
+ public static final int ICON_NOFOLDER = R.drawable.ic_folder_open_grey600_24dp;
+ @DrawableRes
+ public static final int ICON_SUB_FOLDER = R.drawable.ic_folder_grey600_18dp;
+ @DrawableRes
+ public static final int ICON_MULTIPLE = R.drawable.ic_create_new_folder_grey600_24dp;
+ @DrawableRes
+ public static final int ICON_MULTIPLE_OPEN = R.drawable.ic_folder_grey600_24dp;
+ @DrawableRes
+ public static final int ICON_SUB_MULTIPLE = R.drawable.ic_create_new_folder_grey600_18dp;
+
+ public void applyBrand(int mainColor, int textColor) {
+ this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor);
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ private List<NavigationItem> items = new ArrayList<>();
+ private String selectedItem = null;
+ @NonNull
+ private final NavigationClickListener navigationClickListener;
+
+ public NavigationAdapter(@NonNull Context context, @NonNull NavigationClickListener navigationClickListener) {
+ this.context = context;
+ this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context));
+ this.navigationClickListener = navigationClickListener;
+ }
+
+ @NonNull
+ @Override
+ public NavigationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new NavigationViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false), navigationClickListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull NavigationViewHolder holder, int position) {
+ holder.bind(items.get(position), mainColor, selectedItem);
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+ public void setItems(@NonNull List<NavigationItem> items) {
+ for (NavigationItem item : items) {
+ if (TextUtils.isEmpty(item.label)) {
+ item.id = MainActivity.ADAPTER_KEY_UNCATEGORIZED;
+ item.label = context.getString(R.string.action_uncategorized);
+ item.icon = NavigationAdapter.ICON_NOFOLDER;
+ item.type = UNCATEGORIZED;
+ break;
+ }
+ }
+ this.items = items;
+ notifyDataSetChanged();
+ }
+
+ public void setSelectedItem(String id) {
+ selectedItem = id;
+ notifyDataSetChanged();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationClickListener.java
new file mode 100644
index 00000000..0e21d85e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationClickListener.java
@@ -0,0 +1,7 @@
+package it.niedermann.owncloud.notes.main.navigation;
+
+public interface NavigationClickListener {
+ void onItemClick(NavigationItem item);
+
+ void onIconClick(NavigationItem item);
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationItem.java b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationItem.java
new file mode 100644
index 00000000..25ea4abb
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationItem.java
@@ -0,0 +1,96 @@
+package it.niedermann.owncloud.notes.main.navigation;
+
+import android.text.TextUtils;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType;
+
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
+
+public class NavigationItem {
+ @NonNull
+ public String id;
+ @NonNull
+ public String label;
+ @DrawableRes
+ public int icon;
+ @Nullable
+ public Integer count;
+ @Nullable
+ public ENavigationCategoryType type;
+
+ public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon) {
+ this.id = id;
+ this.label = label;
+ this.type = TextUtils.isEmpty(label) ? UNCATEGORIZED : null;
+ this.count = count;
+ this.icon = icon;
+ }
+
+ public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon, @NonNull ENavigationCategoryType type) {
+ this.id = id;
+ this.label = label;
+ this.type = type;
+ this.count = count;
+ this.icon = icon;
+ }
+
+ public static class CategoryNavigationItem extends NavigationItem {
+ public long accountId;
+ @NonNull
+ public String category;
+
+ public CategoryNavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon, long accountId, @NonNull String category) {
+ super(id, label, count, icon, ENavigationCategoryType.DEFAULT_CATEGORY);
+ this.accountId = accountId;
+ this.category = category;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CategoryNavigationItem)) return false;
+ if (!super.equals(o)) return false;
+
+ CategoryNavigationItem that = (CategoryNavigationItem) o;
+
+ if (accountId != that.accountId) return false;
+ return category.equals(that.category);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (int) (accountId ^ (accountId >>> 32));
+ result = 31 * result + category.hashCode();
+ return result;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof NavigationItem)) return false;
+
+ NavigationItem that = (NavigationItem) o;
+
+ if (icon != that.icon) return false;
+ if (!id.equals(that.id)) return false;
+ if (!label.equals(that.label)) return false;
+ if (count != null ? !count.equals(that.count) : that.count != null) return false;
+ return type == that.type;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id.hashCode();
+ result = 31 * result + label.hashCode();
+ result = 31 * result + icon;
+ result = 31 * result + (count != null ? count.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationViewHolder.java
new file mode 100644
index 00000000..eaf40dbc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/navigation/NavigationViewHolder.java
@@ -0,0 +1,63 @@
+package it.niedermann.owncloud.notes.main.navigation;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
+import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+
+import static java.util.Objects.requireNonNull;
+
+class NavigationViewHolder extends RecyclerView.ViewHolder {
+ @NonNull
+ private final View view;
+
+ @NonNull
+ private final TextView name;
+ @NonNull
+ private final TextView count;
+ @NonNull
+ private final ImageView icon;
+
+ private NavigationItem currentItem;
+
+ NavigationViewHolder(@NonNull View itemView, @NonNull final NavigationClickListener navigationClickListener) {
+ super(itemView);
+ view = itemView;
+ ItemNavigationBinding binding = ItemNavigationBinding.bind(view);
+ this.name = binding.navigationItemLabel;
+ this.count = binding.navigationItemCount;
+ this.icon = binding.navigationItemIcon;
+ icon.setOnClickListener(view -> navigationClickListener.onIconClick(currentItem));
+ itemView.setOnClickListener(view -> navigationClickListener.onItemClick(currentItem));
+ }
+
+ public void bind(@NonNull NavigationItem item, @ColorInt int mainColor, String selectedItem) {
+ currentItem = item;
+ boolean isSelected = item.id.equals(selectedItem);
+ name.setText(NoteUtil.extendCategory(item.label));
+ count.setVisibility(item.count == null ? View.GONE : View.VISIBLE);
+ count.setText(String.valueOf(item.count));
+ if (item.icon > 0) {
+ icon.setImageDrawable(DrawableCompat.wrap(requireNonNull(ContextCompat.getDrawable(icon.getContext(), item.icon))));
+ icon.setVisibility(View.VISIBLE);
+ } else {
+ icon.setVisibility(View.GONE);
+ }
+ int textColor = isSelected ? mainColor : view.getResources().getColor(R.color.fg_default);
+
+ name.setTextColor(textColor);
+ count.setTextColor(textColor);
+ icon.setColorFilter(isSelected ? textColor : 0);
+
+ view.setSelected(isSelected);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/slots/SlotterUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/SlotterUtil.java
new file mode 100644
index 00000000..206ee9ca
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/SlotterUtil.java
@@ -0,0 +1,74 @@
+package it.niedermann.owncloud.notes.main.slots;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.main.items.section.SectionItem;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.Item;
+import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+
+public class SlotterUtil {
+
+ private SlotterUtil() {
+ // Util class
+ }
+
+ @NonNull
+ public static List<Item> fillListByCategory(@NonNull List<Note> noteList, @Nullable String currentCategory) {
+ List<Item> itemList = new ArrayList<>();
+ for (Note note : noteList) {
+ if (currentCategory != null && !currentCategory.equals(note.getCategory())) {
+ itemList.add(new SectionItem(NoteUtil.extendCategory(note.getCategory())));
+ }
+
+ itemList.add(note);
+ currentCategory = note.getCategory();
+ }
+ return itemList;
+ }
+
+ @NonNull
+ public static List<Item> fillListByTime(@NonNull Context context, @NonNull List<Note> noteList) {
+ List<Item> itemList = new ArrayList<>();
+ Timeslotter timeslotter = new Timeslotter(context);
+ String lastTimeslot = null;
+ for (int i = 0; i < noteList.size(); i++) {
+ Note currentNote = noteList.get(i);
+ String timeslot = timeslotter.getTimeslot(currentNote);
+ if (i > 0 && !timeslot.equals(lastTimeslot)) {
+ itemList.add(new SectionItem(timeslot));
+ }
+ itemList.add(currentNote);
+ lastTimeslot = timeslot;
+ }
+
+ return itemList;
+ }
+
+ @NonNull
+ public static List<Item> fillListByInitials(@NonNull Context context, @NonNull List<Note> noteList) {
+ List<Item> itemList = new ArrayList<>();
+ String lastInitials = null;
+ for (int i = 0; i < noteList.size(); i++) {
+ Note currentNote = noteList.get(i);
+ String initials = currentNote.getTitle().substring(0, 1).toUpperCase();
+ if (!initials.matches("[A-Z\\u00C0-\\u00DF]")) {
+ initials = initials.matches("[\\u0250-\\uFFFF]") ? context.getString(R.string.simple_other) : "#";
+ }
+ if (i > 0 && !initials.equals(lastInitials)) {
+ itemList.add(new SectionItem(initials));
+ }
+ itemList.add(currentNote);
+ lastInitials = initials;
+ }
+
+ return itemList;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslot.java b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslot.java
new file mode 100644
index 00000000..562a2985
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslot.java
@@ -0,0 +1,22 @@
+package it.niedermann.owncloud.notes.main.slots;
+
+import java.util.Calendar;
+
+public class Timeslot {
+ private final String label;
+ private final Calendar time;
+
+ Timeslot(String label, int month, int day) {
+ this.label = label;
+ this.time = Calendar.getInstance();
+ this.time.set(this.time.get(Calendar.YEAR), month, day, 0, 0, 0);
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public Calendar getTime() {
+ return time;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslotter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslotter.java
new file mode 100644
index 00000000..988311bc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/slots/Timeslotter.java
@@ -0,0 +1,53 @@
+package it.niedermann.owncloud.notes.main.slots;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+
+public class Timeslotter {
+ private final List<Timeslot> timeslots = new ArrayList<>();
+ private final Calendar lastYear;
+ private final Context context;
+
+ public Timeslotter(@NonNull Context context) {
+ this.context = context;
+ Calendar now = Calendar.getInstance();
+ int month = now.get(Calendar.MONTH);
+ int day = now.get(Calendar.DAY_OF_MONTH);
+ int offsetWeekStart = (now.get(Calendar.DAY_OF_WEEK) - now.getFirstDayOfWeek() + 7) % 7;
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_today), month, day));
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_yesterday), month, day - 1));
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_week), month, day - offsetWeekStart));
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_week), month, day - offsetWeekStart - 7));
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_month), month, 1));
+ timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_month), month - 1, 1));
+ lastYear = Calendar.getInstance();
+ lastYear.set(now.get(Calendar.YEAR) - 1, 0, 1, 0, 0, 0);
+ }
+
+ public String getTimeslot(Note note) {
+ if (note.getFavorite()) {
+ return "";
+ }
+ Calendar modified = note.getModified();
+ for (Timeslot timeslot : timeslots) {
+ if (!modified.before(timeslot.getTime())) {
+ return timeslot.getLabel();
+ }
+ }
+ if (!modified.before(this.lastYear)) {
+ // use YEAR and MONTH in a format based on current locale
+ return DateUtils.formatDateTime(context, modified.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NO_MONTH_DAY);
+ } else {
+ return Integer.toString(modified.get(Calendar.YEAR));
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java
index 8d04e34e..c1a4e139 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java
@@ -1,7 +1,6 @@
package it.niedermann.owncloud.notes.manageaccounts;
import android.view.LayoutInflater;
-import android.view.MenuInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -13,28 +12,27 @@ import java.util.ArrayList;
import java.util.List;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class ManageAccountAdapter extends RecyclerView.Adapter<ManageAccountViewHolder> {
@Nullable
- private LocalAccount currentLocalAccount = null;
+ private Account currentLocalAccount = null;
@NonNull
- private final List<LocalAccount> localAccounts = new ArrayList<>();
+ private final List<Account> localAccounts = new ArrayList<>();
@NonNull
- private final Consumer<LocalAccount> onAccountClick;
+ private final Consumer<Account> onAccountClick;
@NonNull
- private final Consumer<LocalAccount> onAccountDelete;
+ private final Consumer<Account> onAccountDelete;
@NonNull
- Consumer<LocalAccount> onChangeNotesPath;
+ Consumer<Account> onChangeNotesPath;
@NonNull
- Consumer<LocalAccount> onChangeFileSuffix;
+ Consumer<Account> onChangeFileSuffix;
- public ManageAccountAdapter(@NonNull Consumer<LocalAccount> onAccountClick,
- @NonNull Consumer<LocalAccount> onAccountDelete,
- @NonNull Consumer<LocalAccount> onChangeNotesPath,
- @NonNull Consumer<LocalAccount> onChangeFileSuffix) {
+ public ManageAccountAdapter(@NonNull Consumer<Account> onAccountClick,
+ @NonNull Consumer<Account> onAccountDelete,
+ @NonNull Consumer<Account> onChangeNotesPath,
+ @NonNull Consumer<Account> onChangeFileSuffix) {
this.onAccountClick = onAccountClick;
this.onAccountDelete = onAccountDelete;
this.onChangeNotesPath = onChangeNotesPath;
@@ -55,7 +53,7 @@ public class ManageAccountAdapter extends RecyclerView.Adapter<ManageAccountView
@Override
public void onBindViewHolder(@NonNull ManageAccountViewHolder holder, int position) {
- final LocalAccount localAccount = localAccounts.get(position);
+ final Account localAccount = localAccounts.get(position);
holder.bind(localAccount, (localAccountClicked) -> {
setCurrentLocalAccount(localAccountClicked);
onAccountClick.accept(localAccountClicked);
@@ -76,13 +74,13 @@ public class ManageAccountAdapter extends RecyclerView.Adapter<ManageAccountView
return localAccounts.size();
}
- public void setLocalAccounts(@NonNull List<LocalAccount> localAccounts) {
+ public void setLocalAccounts(@NonNull List<Account> localAccounts) {
this.localAccounts.clear();
this.localAccounts.addAll(localAccounts);
notifyDataSetChanged();
}
- public void setCurrentLocalAccount(@Nullable LocalAccount currentLocalAccount) {
+ public void setCurrentLocalAccount(@Nullable Account currentLocalAccount) {
this.currentLocalAccount = currentLocalAccount;
notifyDataSetChanged();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
index 910dccc0..1d01387a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
@@ -1,5 +1,6 @@
package it.niedermann.owncloud.notes.manageaccounts;
+import android.graphics.Color;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.view.LayoutInflater;
@@ -22,7 +23,7 @@ import java.util.stream.Stream;
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@@ -38,11 +39,11 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
}
public void bind(
- @NonNull LocalAccount localAccount,
- @NonNull Consumer<LocalAccount> onAccountClick,
- @NonNull Consumer<LocalAccount> onAccountDelete,
- @NonNull Consumer<LocalAccount> onChangeNotesPath,
- @NonNull Consumer<LocalAccount> onChangeFileSuffix,
+ @NonNull Account localAccount,
+ @NonNull Consumer<Account> onAccountClick,
+ @NonNull Consumer<Account> onAccountDelete,
+ @NonNull Consumer<Account> onChangeNotesPath,
+ @NonNull Consumer<Account> onChangeFileSuffix,
boolean isCurrentAccount
) {
binding.accountName.setText(localAccount.getUserName());
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java
index b6950c85..408c1f2d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java
@@ -14,7 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.LiveData;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
@@ -32,18 +32,19 @@ import it.niedermann.owncloud.notes.databinding.ActivityManageAccountsBinding;
import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
import it.niedermann.owncloud.notes.persistence.NotesClient;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.ServerSettings;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
public class ManageAccountsActivity extends LockedActivity {
private ActivityManageAccountsBinding binding;
private ManageAccountAdapter adapter;
private NotesDatabase db = null;
- private final List<LocalAccount> localAccounts = new ArrayList<>();
+ private final List<Account> localAccounts = new ArrayList<>();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -56,45 +57,54 @@ public class ManageAccountsActivity extends LockedActivity {
db = NotesDatabase.getInstance(this);
- localAccounts.clear();
- localAccounts.addAll(db.getAccounts());
-
- adapter = new ManageAccountAdapter(
- (localAccount) -> SingleAccountHelper.setCurrentAccount(getApplicationContext(), localAccount.getAccountName()),
- this::onAccountDelete,
- this::onChangeNotesPath,
- this::onChangeFileSuffix
- );
- adapter.setLocalAccounts(localAccounts);
- try {
- final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(this);
- if (ssoAccount != null) {
- adapter.setCurrentLocalAccount(db.getLocalAccountByAccountName(ssoAccount.name));
+ distinctUntilChanged(db.getAccountDao().getAccounts$()).observe(this, (localAccounts) -> {
+
+ this.localAccounts.clear();
+ this.localAccounts.addAll(localAccounts);
+
+ adapter = new ManageAccountAdapter(
+ (localAccount) -> SingleAccountHelper.setCurrentAccount(getApplicationContext(), localAccount.getAccountName()),
+ this::onAccountDelete,
+ this::onChangeNotesPath,
+ this::onChangeFileSuffix
+ );
+ adapter.setLocalAccounts(localAccounts);
+ try {
+ final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(this);
+ if (ssoAccount != null) {
+ new Thread(() -> {
+ final Account account = db.getAccountDao().getAccountByName(ssoAccount.name);
+ runOnUiThread(() -> adapter.setCurrentLocalAccount(account));
+ }).start();
+ }
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
}
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- e.printStackTrace();
- }
- binding.accounts.setAdapter(adapter);
+ binding.accounts.setAdapter(adapter);
+ });
}
- private void onAccountDelete(@NonNull LocalAccount localAccount) {
- db.deleteAccount(localAccount);
- for (LocalAccount temp : localAccounts) {
- if (temp.getId() == localAccount.getId()) {
- localAccounts.remove(temp);
- break;
+ private void onAccountDelete(@NonNull Account localAccount) {
+ final LiveData<Void> deleteLiveData = db.deleteAccount(localAccount);
+ deleteLiveData.observe(this, (v) -> {
+ for (Account temp : localAccounts) {
+ if (temp.getId() == localAccount.getId()) {
+ localAccounts.remove(temp);
+ break;
+ }
}
- }
- if (localAccounts.size() > 0) {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), localAccounts.get(0).getAccountName());
- adapter.setCurrentLocalAccount(localAccounts.get(0));
- } else {
- setResult(AppCompatActivity.RESULT_FIRST_USER);
- finish();
- }
+ if (localAccounts.size() > 0) {
+ SingleAccountHelper.setCurrentAccount(getApplicationContext(), localAccounts.get(0).getAccountName());
+ adapter.setCurrentLocalAccount(localAccounts.get(0));
+ } else {
+ SingleAccountHelper.setCurrentAccount(getApplicationContext(), null);
+ finish();
+ }
+ deleteLiveData.removeObservers(this);
+ });
}
- private void onChangeNotesPath(@NonNull LocalAccount localAccount) {
+ private void onChangeNotesPath(@NonNull Account localAccount) {
final NotesClient client = NotesClient.newInstance(localAccount.getPreferredApiVersion(), getApplicationContext());
final EditText editText = new EditText(this);
editText.setEnabled(false);
@@ -125,7 +135,7 @@ public class ManageAccountsActivity extends LockedActivity {
}).start();
}
- private void onChangeFileSuffix(@NonNull LocalAccount localAccount) {
+ private void onChangeFileSuffix(@NonNull Account localAccount) {
final NotesClient client = NotesClient.newInstance(localAccount.getPreferredApiVersion(), getApplicationContext());
final Spinner spinner = new Spinner(this);
spinner.setEnabled(false);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java
deleted file mode 100644
index 46d36e1c..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java
+++ /dev/null
@@ -1,214 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import it.niedermann.owncloud.notes.persistence.migration.Migration_10_11;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_11_12;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_12_13;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_13_14;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_14_15;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_15_16;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_16_17;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_17_18;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_18_19;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_4_5;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_5_6;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_6_7;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_7_8;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_8_9;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
-import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-
-abstract class AbstractNotesDatabase extends SQLiteOpenHelper {
-
- private static final int database_version = 19;
- @NonNull
- protected final Context context;
-
- protected static final String database_name = "OWNCLOUD_NOTES";
- protected static final String table_notes = "NOTES";
- protected static final String table_accounts = "ACCOUNTS";
- protected static final String table_category = "CATEGORIES";
- protected static final String table_widget_single_notes = "WIDGET_SINGLE_NOTES";
- protected static final String table_widget_note_list = "WIDGET_NOTE_LISTS";
-
- protected static final String key_id = "ID";
- protected static final String key_url = "URL";
- protected static final String key_account_name = "ACCOUNT_NAME";
- protected static final String key_username = "USERNAME";
- protected static final String key_account_id = "ACCOUNT_ID";
- protected static final String key_note_id = "NOTE_ID";
- protected static final String key_remote_id = "REMOTEID";
- protected static final String key_status = "STATUS";
- protected static final String key_title = "TITLE";
- protected static final String key_modified = "MODIFIED";
- protected static final String key_content = "CONTENT";
- protected static final String key_excerpt = "EXCERPT";
- protected static final String key_favorite = "FAVORITE";
- protected static final String key_category = "CATEGORY";
- protected static final String key_etag = "ETAG";
- protected static final String key_capabilities_etag = "CAPABILITIES_ETAG";
- protected static final String key_color = "COLOR";
- protected static final String key_text_color = "TEXT_COLOR";
- protected static final String key_api_version = "API_VERSION";
- protected static final String key_category_id = "CATEGORY_ID";
- protected static final String key_category_title = "CATEGORY_TITLE";
- protected static final String key_category_account_id = "CATEGORY_ACCOUNT_ID";
- protected static final String key_theme_mode = "THEME_MODE";
- protected static final String key_mode = "MODE";
- protected static final String key_scroll_y = "SCROLL_Y";
- protected static final String key_category_sorting_method = "CATEGORY_SORTING_METHOD";
-
- protected AbstractNotesDatabase(@NonNull Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory) {
- super(context, name, factory, database_version);
- this.context = context;
- }
-
-
- @NonNull
- public Context getContext() {
- return context;
- }
-
- /**
- * Creates initial the Database
- *
- * @param db Database
- */
- @Override
- public void onCreate(SQLiteDatabase db) {
- createAccountTable(db);
- createNotesTable(db);
- createCategoryTable(db);
- createWidgetSingleNoteTable(db);
- createWidgetNoteListTable(db);
- }
-
- private void createNotesTable(@NonNull SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + table_notes + " ( " +
- key_id + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- key_remote_id + " INTEGER, " +
- key_account_id + " INTEGER, " + // TODO NOT NULL
- key_status + " VARCHAR(50), " +
- key_title + " TEXT, " +
- key_modified + " INTEGER DEFAULT 0, " +
- key_content + " TEXT, " +
- key_favorite + " INTEGER DEFAULT 0, " +
- key_category + " INTEGER, " +
- key_etag + " TEXT," +
- key_excerpt + " TEXT NOT NULL DEFAULT '', " +
- key_scroll_y + " INTEGER DEFAULT 0, " +
- "FOREIGN KEY(" + key_category + ") REFERENCES " + table_category + "(" + key_category_id + "), " +
- "FOREIGN KEY(" + key_account_id + ") REFERENCES " + table_accounts + "(" + key_id + "))");
- DatabaseIndexUtil.createIndex(db, table_notes, key_remote_id, key_account_id, key_status, key_favorite, key_category, key_modified);
- }
-
- private void createAccountTable(@NonNull SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + table_accounts + " ( " +
- key_id + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- key_url + " TEXT, " +
- key_username + " TEXT, " +
- key_account_name + " TEXT UNIQUE, " +
- key_etag + " TEXT, " +
- key_modified + " INTEGER, " +
- key_api_version + " TEXT, " +
- key_color + " VARCHAR(6) NOT NULL DEFAULT '000000', " +
- key_text_color + " VARCHAR(6) NOT NULL DEFAULT '0082C9', " +
- key_capabilities_etag + " TEXT);");
- DatabaseIndexUtil.createIndex(db, table_accounts, key_url, key_username, key_account_name, key_etag, key_modified);
- }
-
- private void createCategoryTable(@NonNull SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + table_category + "(" +
- key_category_id + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- key_category_account_id + " INTEGER NOT NULL, " +
- key_category_title + " TEXT NOT NULL, " +
- key_category_sorting_method + " INTEGER DEFAULT 0, " +
- " UNIQUE( " + key_category_account_id + " , " + key_category_title + "), " +
- " FOREIGN KEY(" + key_category_account_id + ") REFERENCES " + table_accounts + "(" + key_id + "));");
- DatabaseIndexUtil.createIndex(db, table_category, key_category_id, key_category_account_id, key_category_title, key_category_sorting_method);
- }
-
- private void createWidgetSingleNoteTable(@NonNull SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + table_widget_single_notes + " ( " +
- key_id + " INTEGER PRIMARY KEY, " +
- key_account_id + " INTEGER, " +
- key_note_id + " INTEGER, " +
- key_theme_mode + " INTEGER NOT NULL, " +
- "FOREIGN KEY(" + key_account_id + ") REFERENCES " + table_accounts + "(" + key_id + "), " +
- "FOREIGN KEY(" + key_note_id + ") REFERENCES " + table_notes + "(" + key_id + "))");
- }
-
- private void createWidgetNoteListTable(@NonNull SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + table_widget_note_list + " ( " +
- key_id + " INTEGER PRIMARY KEY, " +
- key_account_id + " INTEGER, " +
- key_category_id + " INTEGER, " +
- key_mode + " INTEGER NOT NULL, " +
- key_theme_mode + " INTEGER NOT NULL, " +
- "FOREIGN KEY(" + key_account_id + ") REFERENCES " + table_accounts + "(" + key_id + "), " +
- "FOREIGN KEY(" + key_category_id + ") REFERENCES " + table_category + "(" + key_category_id + "))");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- switch (oldVersion) {
- case 0:
- case 1:
- case 2:
- case 3:
- recreateDatabase(db);
- return;
- case 4:
- new Migration_4_5(db);
- case 5:
- new Migration_5_6(db);
- case 6:
- new Migration_6_7(db);
- case 7:
- new Migration_7_8(db);
- case 8:
- new Migration_8_9(db, context, this::recreateDatabase, this::notifyWidgets);
- case 9:
- new Migration_9_10(db);
- case 10:
- new Migration_10_11(context);
- case 11:
- new Migration_11_12(db, context);
- case 12:
- new Migration_12_13(db, context);
- case 13:
- new Migration_13_14(db, context, this::notifyWidgets);
- case 14:
- new Migration_14_15(db);
- case 15:
- new Migration_15_16(db, context, this::notifyWidgets);
- case 16:
- new Migration_16_17(db);
- case 17:
- new Migration_17_18(db);
- case 18:
- new Migration_18_19(context);
- }
- }
-
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- recreateDatabase(db);
- }
-
- private void recreateDatabase(SQLiteDatabase db) {
- DatabaseIndexUtil.dropIndexes(db);
- db.execSQL("DROP TABLE IF EXISTS " + table_notes);
- db.execSQL("DROP TABLE IF EXISTS " + table_accounts);
- db.execSQL("DROP TABLE IF EXISTS " + table_category);
- onCreate(db);
- }
-
- protected abstract void notifyWidgets();
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
index 26e18311..a9590225 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
@@ -20,8 +20,8 @@ import java.net.HttpURLConnection;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
public class CapabilitiesWorker extends Worker {
@@ -43,13 +43,13 @@ public class CapabilitiesWorker extends Worker {
@Override
public Result doWork() {
final NotesDatabase db = NotesDatabase.getInstance(getApplicationContext());
- for (LocalAccount account : db.getAccounts()) {
+ for (Account account : db.getAccountDao().getAccounts()) {
try {
final SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName());
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag());
- db.updateCapabilitiesETag(account.getId(), capabilities.getETag());
- db.updateBrand(account.getId(), capabilities);
+ db.getAccountDao().updateCapabilitiesETag(account.getId(), capabilities.getETag());
+ db.getAccountDao().updateBrand(account.getId(), capabilities.getColor(), capabilities.getTextColor());
db.updateApiVersion(account.getId(), capabilities.getApiVersion());
Log.i(TAG, capabilities.toString());
} catch (Exception e) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java
deleted file mode 100644
index 0380b0be..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java
+++ /dev/null
@@ -1,170 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.text.format.DateUtils;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.shared.model.Category;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.Item;
-import it.niedermann.owncloud.notes.main.items.section.SectionItem;
-import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
-import it.niedermann.owncloud.notes.shared.util.NoteUtil;
-
-public class LoadNotesListTask extends AsyncTask<Void, Void, List<Item>> {
-
- private final Context context;
- private final NotesLoadedListener callback;
- private final Category category;
- private final CharSequence searchQuery;
- private final long accountId;
-
- public LoadNotesListTask(long accountId, @NonNull Context context, @NonNull NotesLoadedListener callback, @NonNull Category category, @Nullable CharSequence searchQuery) {
- this.context = context;
- this.callback = callback;
- this.category = category;
- this.searchQuery = searchQuery;
- this.accountId = accountId;
- }
-
- @Override
- protected List<Item> doInBackground(Void... voids) {
- List<DBNote> noteList;
- NotesDatabase db = NotesDatabase.getInstance(context);
- CategorySortingMethod sortingMethod = db.getCategoryOrder(accountId, category);
- noteList = db.searchNotes(accountId, searchQuery, category.category, category.favorite, sortingMethod);
-
- if (category.category == null) {
- if (sortingMethod == CategorySortingMethod.SORT_MODIFIED_DESC) {
- return fillListByTime(noteList);
- } else {
- return fillListByInitials(noteList);
- }
- } else {
- return fillListByCategory(noteList);
- }
- }
-
- @NonNull
- @WorkerThread
- private List<Item> fillListByCategory(@NonNull List<DBNote> noteList) {
- List<Item> itemList = new ArrayList<>();
- String currentCategory = category.category;
- for (DBNote note : noteList) {
- if (currentCategory != null && !currentCategory.equals(note.getCategory())) {
- itemList.add(new SectionItem(NoteUtil.extendCategory(note.getCategory())));
- }
-
- itemList.add(note);
- currentCategory = note.getCategory();
- }
- return itemList;
- }
-
- @NonNull
- @WorkerThread
- private List<Item> fillListByTime(@NonNull List<DBNote> noteList) {
- List<Item> itemList = new ArrayList<>();
- Timeslotter timeslotter = new Timeslotter();
- String lastTimeslot = null;
- for (int i = 0; i < noteList.size(); i++) {
- DBNote currentNote = noteList.get(i);
- String timeslot = timeslotter.getTimeslot(currentNote);
- if (i > 0 && !timeslot.equals(lastTimeslot)) {
- itemList.add(new SectionItem(timeslot));
- }
- itemList.add(currentNote);
- lastTimeslot = timeslot;
- }
-
- return itemList;
- }
-
- @NonNull
- @WorkerThread
- private List<Item> fillListByInitials(@NonNull List<DBNote> noteList) {
- List<Item> itemList = new ArrayList<>();
- String lastInitials = null;
- for (int i = 0; i < noteList.size(); i++) {
- DBNote currentNote = noteList.get(i);
- String initials = currentNote.getTitle().substring(0, 1).toUpperCase();
- if (!initials.matches("[A-Z\\u00C0-\\u00DF]")) {
- initials = initials.matches("[\\u0250-\\uFFFF]") ? context.getString(R.string.simple_other) : "#";
- }
- if (i > 0 && !initials.equals(lastInitials)) {
- itemList.add(new SectionItem(initials));
- }
- itemList.add(currentNote);
- lastInitials = initials;
- }
-
- return itemList;
- }
-
- @Override
- protected void onPostExecute(List<Item> items) {
- callback.onNotesLoaded(items, category.category == null, searchQuery);
- }
-
- public interface NotesLoadedListener {
- void onNotesLoaded(List<Item> notes, boolean showCategory, CharSequence searchQuery);
- }
-
- private class Timeslotter {
- private final List<Timeslot> timeslots = new ArrayList<>();
- private final Calendar lastYear;
-
- Timeslotter() {
- Calendar now = Calendar.getInstance();
- int month = now.get(Calendar.MONTH);
- int day = now.get(Calendar.DAY_OF_MONTH);
- int offsetWeekStart = (now.get(Calendar.DAY_OF_WEEK) - now.getFirstDayOfWeek() + 7) % 7;
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_today), month, day));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_yesterday), month, day - 1));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_week), month, day - offsetWeekStart));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_week), month, day - offsetWeekStart - 7));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_month), month, 1));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_month), month - 1, 1));
- lastYear = Calendar.getInstance();
- lastYear.set(now.get(Calendar.YEAR) - 1, 0, 1, 0, 0, 0);
- }
-
- private String getTimeslot(DBNote note) {
- if (note.isFavorite()) {
- return "";
- }
- Calendar modified = note.getModified();
- for (Timeslot timeslot : timeslots) {
- if (!modified.before(timeslot.time)) {
- return timeslot.label;
- }
- }
- if (!modified.before(this.lastYear)) {
- // use YEAR and MONTH in a format based on current locale
- return DateUtils.formatDateTime(context, modified.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NO_MONTH_DAY);
- } else {
- return Integer.toString(modified.get(Calendar.YEAR));
- }
- }
-
- private class Timeslot {
- private final String label;
- private final Calendar time;
-
- Timeslot(String label, int month, int day) {
- this.label = label;
- this.time = Calendar.getInstance();
- this.time.set(this.time.get(Calendar.YEAR), month, day, 0, 0, 0);
- }
- }
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java
deleted file mode 100644
index da8feb47..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java
+++ /dev/null
@@ -1,529 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.accounts.NetworkErrorException;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
-import com.nextcloud.android.sso.exceptions.TokenMismatchException;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
-import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.DBStatus;
-import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
-import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
-import it.niedermann.owncloud.notes.shared.util.SSOUtil;
-import it.niedermann.owncloud.notes.shared.model.ServerResponse;
-
-import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
-import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
-
-/**
- * Helps to synchronize the Database to the Server.
- */
-public class NoteServerSyncHelper {
-
- private static final String TAG = NoteServerSyncHelper.class.getSimpleName();
-
- private static NoteServerSyncHelper instance;
-
- private final NotesDatabase db;
- private final Context context;
-
- // Track network connection changes using a BroadcastReceiver
- private boolean isSyncPossible = false;
- private boolean networkConnected = false;
- private String syncOnlyOnWifiKey;
- private boolean syncOnlyOnWifi;
-
- /**
- * @see <a href="https://stackoverflow.com/a/3104265">Do not make this a local variable.</a>
- */
- @SuppressWarnings("FieldCanBeLocal")
- private SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = (SharedPreferences prefs, String key) -> {
- if (syncOnlyOnWifiKey.equals(key)) {
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
- updateNetworkStatus();
- }
- };
-
- private final BroadcastReceiver networkReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateNetworkStatus();
- if (isSyncPossible() && SSOUtil.isConfigured(context)) {
- try {
- scheduleSync(SingleAccountHelper.getCurrentSingleSignOnAccount(context), false);
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- Log.v(TAG, "Can not select current SingleSignOn account after network changed, do not sync.");
- }
- }
- }
- };
-
- // current state of the synchronization
- private final Map<String, Boolean> syncActive = new HashMap<>();
- private final Map<String, Boolean> syncScheduled = new HashMap<>();
-
- // list of callbacks for both parts of synchronziation
- private final Map<String, List<ISyncCallback>> callbacksPush = new HashMap<>();
- private final Map<String, List<ISyncCallback>> callbacksPull = new HashMap<>();
-
- private NoteServerSyncHelper(NotesDatabase db) {
- this.db = db;
- this.context = db.getContext();
- this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
-
- // Registers BroadcastReceiver to track network connection changes.
- context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
- prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
-
- updateNetworkStatus();
- }
-
- /**
- * Get (or create) instance from NoteServerSyncHelper.
- * This has to be a singleton in order to realize correct registering and unregistering of
- * the BroadcastReceiver, which listens on changes of network connectivity.
- *
- * @param dbHelper NoteSQLiteOpenHelper
- * @return NoteServerSyncHelper
- */
- public static synchronized NoteServerSyncHelper getInstance(NotesDatabase dbHelper) {
- if (instance == null) {
- instance = new NoteServerSyncHelper(dbHelper);
- }
- return instance;
- }
-
- @Override
- protected void finalize() throws Throwable {
- context.getApplicationContext().unregisterReceiver(networkReceiver);
- super.finalize();
- }
-
- /**
- * Synchronization is only possible, if there is an active network connection.
- * <p>
- * This method respects the user preference "Sync on Wi-Fi only".
- * <p>
- * NoteServerSyncHelper observes changes in the network connection.
- * The current state can be retrieved with this method.
- *
- * @return true if sync is possible, otherwise false.
- */
- public boolean isSyncPossible() {
- return isSyncPossible;
- }
-
- public boolean isNetworkConnected() {
- return networkConnected;
- }
-
- public boolean isSyncOnlyOnWifi() {
- return syncOnlyOnWifi;
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
- */
- public void addCallbackPush(SingleSignOnAccount ssoAccount, ISyncCallback callback) {
- if (ssoAccount == null) {
- Log.i(TAG, "ssoAccount is null. Is this a local account?");
- callback.onScheduled();
- callback.onFinish();
- } else {
- if (!callbacksPush.containsKey(ssoAccount.name)) {
- callbacksPush.put(ssoAccount.name, new ArrayList<>());
- }
- Objects.requireNonNull(callbacksPush.get(ssoAccount.name)).add(callback);
- }
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
- */
- public void addCallbackPull(SingleSignOnAccount ssoAccount, ISyncCallback callback) {
- if (ssoAccount == null) {
- Log.i(TAG, "ssoAccount is null. Is this a local account?");
- callback.onScheduled();
- callback.onFinish();
- } else {
- if (!callbacksPull.containsKey(ssoAccount.name)) {
- callbacksPull.put(ssoAccount.name, new ArrayList<>());
- }
- Objects.requireNonNull(callbacksPull.get(ssoAccount.name)).add(callback);
- }
- }
-
- /**
- * Schedules a synchronization and start it directly, if the network is connected and no
- * synchronization is currently running.
- *
- * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
- */
- public void scheduleSync(SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
- if (ssoAccount == null) {
- Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
- } else {
- if (syncActive.get(ssoAccount.name) == null) {
- syncActive.put(ssoAccount.name, false);
- }
- Log.d(TAG, "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (Boolean.TRUE.equals(syncActive.get(ssoAccount.name)) ? "sync active" : "sync NOT active") + ") ...");
- if (isSyncPossible() && (!Boolean.TRUE.equals(syncActive.get(ssoAccount.name)) || onlyLocalChanges)) {
- Log.d(TAG, "... starting now");
- final LocalAccount localAccount = db.getLocalAccountByAccountName(ssoAccount.name);
- if (localAccount == null) {
- Log.e(TAG, LocalAccount.class.getSimpleName() + " for ssoAccount \"" + ssoAccount.name + "\" is null. Cannot synchronize.", new IllegalStateException());
- return;
- }
- final NotesClient notesClient = NotesClient.newInstance(localAccount.getPreferredApiVersion(), context);
- final SyncTask syncTask = new SyncTask(notesClient, localAccount, ssoAccount, onlyLocalChanges);
- syncTask.addCallbacks(ssoAccount, callbacksPush.get(ssoAccount.name));
- callbacksPush.put(ssoAccount.name, new ArrayList<>());
- if (!onlyLocalChanges) {
- syncTask.addCallbacks(ssoAccount, callbacksPull.get(ssoAccount.name));
- callbacksPull.put(ssoAccount.name, new ArrayList<>());
- }
- syncTask.execute();
- } else if (!onlyLocalChanges) {
- Log.d(TAG, "... scheduled");
- syncScheduled.put(ssoAccount.name, true);
- if (callbacksPush.containsKey(ssoAccount.name) && callbacksPush.get(ssoAccount.name) != null) {
- final List<ISyncCallback> callbacks = callbacksPush.get(ssoAccount.name);
- if (callbacks != null) {
- for (ISyncCallback callback : callbacks) {
- callback.onScheduled();
- }
- } else {
- Log.w(TAG, "List of push-callbacks was set for account \"" + ssoAccount.name + "\" but it was null");
- }
- }
- } else {
- Log.d(TAG, "... do nothing");
- if (callbacksPush.containsKey(ssoAccount.name) && callbacksPush.get(ssoAccount.name) != null) {
- final List<ISyncCallback> callbacks = callbacksPush.get(ssoAccount.name);
- if (callbacks != null) {
- for (ISyncCallback callback : callbacks) {
- callback.onScheduled();
- }
- } else {
- Log.w(TAG, "List of push-callbacks was set for account \"" + ssoAccount.name + "\" but it was null");
- }
- }
- }
- }
- }
-
- public void updateNetworkStatus() {
- try {
- final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-
- if (connMgr == null) {
- throw new NetworkErrorException("ConnectivityManager is null");
- }
-
- final NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
-
- if (activeInfo == null) {
- throw new NetworkErrorException("NetworkInfo is null");
- }
-
- if (activeInfo.isConnected()) {
- networkConnected = true;
-
- final NetworkInfo networkInfo = connMgr.getNetworkInfo((ConnectivityManager.TYPE_WIFI));
-
- if (networkInfo == null) {
- throw new NetworkErrorException("connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI) is null");
- }
-
- isSyncPossible = !syncOnlyOnWifi || networkInfo.isConnected();
-
- if (isSyncPossible) {
- Log.d(TAG, "Network connection established.");
- } else {
- Log.d(TAG, "Network connected, but not used because only synced on wifi.");
- }
- } else {
- networkConnected = false;
- isSyncPossible = false;
- Log.d(TAG, "No network connection.");
- }
- } catch (NetworkErrorException e) {
- e.printStackTrace();
- networkConnected = false;
- isSyncPossible = false;
- }
- }
-
- /**
- * SyncTask is an AsyncTask which performs the synchronization in a background thread.
- * Synchronization consists of two parts: pushLocalChanges and pullRemoteChanges.
- */
- private class SyncTask extends AsyncTask<Void, Void, SyncResultStatus> {
- @NonNull
- private final NotesClient notesClient;
- @NonNull
- private final LocalAccount localAccount;
- @NonNull
- private final SingleSignOnAccount ssoAccount;
- private final boolean onlyLocalChanges;
- @NonNull
- private final Map<String, List<ISyncCallback>> callbacks = new HashMap<>();
- @NonNull
- private final ArrayList<Throwable> exceptions = new ArrayList<>();
-
- SyncTask(@NonNull NotesClient notesClient, @NonNull LocalAccount localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
- this.notesClient = notesClient;
- this.localAccount = localAccount;
- this.ssoAccount = ssoAccount;
- this.onlyLocalChanges = onlyLocalChanges;
- }
-
- private void addCallbacks(SingleSignOnAccount ssoAccount, List<ISyncCallback> callbacks) {
- this.callbacks.put(ssoAccount.name, callbacks);
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- if (!syncScheduled.containsKey(ssoAccount.name) || syncScheduled.get(ssoAccount.name) == null) {
- syncScheduled.put(ssoAccount.name, false);
- }
- if (!onlyLocalChanges && Boolean.TRUE.equals(syncScheduled.get(ssoAccount.name))) {
- syncScheduled.put(ssoAccount.name, false);
- }
- syncActive.put(ssoAccount.name, true);
- }
-
- @Override
- protected SyncResultStatus doInBackground(Void... voids) {
- Log.i(TAG, "STARTING SYNCHRONIZATION");
- final SyncResultStatus status = new SyncResultStatus();
- status.pushSuccessful = pushLocalChanges();
- if (!onlyLocalChanges) {
- status.pullSuccessful = pullRemoteChanges();
- }
- Log.i(TAG, "SYNCHRONIZATION FINISHED");
- return status;
- }
-
- /**
- * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server.
- */
- private boolean pushLocalChanges() {
- Log.d(TAG, "pushLocalChanges()");
-
- boolean success = true;
- List<DBNote> notes = db.getLocalModifiedNotes(localAccount.getId());
- for (DBNote note : notes) {
- Log.d(TAG, " Process Local Note: " + note);
- try {
- CloudNote remoteNote;
- switch (note.getStatus()) {
- case LOCAL_EDITED:
- Log.v(TAG, " ...create/edit");
- if (note.getRemoteId() > 0) {
- Log.v(TAG, " ...Note has remoteId → try to edit");
- try {
- remoteNote = notesClient.editNote(ssoAccount, note).getNote();
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_FOUND) {
- Log.v(TAG, " ...Note does no longer exist on server → recreate");
- remoteNote = notesClient.createNote(ssoAccount, note).getNote();
- } else {
- throw e;
- }
- }
- } else {
- Log.v(TAG, " ...Note does not have a remoteId yet → create");
- remoteNote = notesClient.createNote(ssoAccount, note).getNote();
- }
- // Please note, that db.updateNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- db.updateNote(localAccount, note.getId(), remoteNote, note);
- break;
- case LOCAL_DELETED:
- if (note.getRemoteId() > 0) {
- Log.v(TAG, " ...delete (from server and local)");
- try {
- notesClient.deleteNote(ssoAccount, note.getRemoteId());
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_FOUND) {
- Log.v(TAG, " ...delete (note has already been deleted remotely)");
- } else {
- throw e;
- }
- }
- } else {
- Log.v(TAG, " ...delete (only local, since it has never been synchronized)");
- }
- // Please note, that db.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- db.deleteNote(note.getId(), DBStatus.LOCAL_DELETED);
- break;
- default:
- throw new IllegalStateException("Unknown State of Note: " + note);
- }
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
- Log.d(TAG, "Server returned HTTP Status Code 304 - Not Modified");
- } else {
- exceptions.add(e);
- success = false;
- }
- } catch (Exception e) {
- if (e instanceof TokenMismatchException) {
- SSOClient.invalidateAPICache(ssoAccount);
- }
- exceptions.add(e);
- success = false;
- }
- }
- return success;
- }
-
- /**
- * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes.
- */
- private boolean pullRemoteChanges() {
- Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
- try {
- final Map<Long, Long> idMap = db.getIdMap(localAccount.getId());
- final ServerResponse.NotesResponse response = notesClient.getNotes(ssoAccount, localAccount.getModified(), localAccount.getEtag());
- List<CloudNote> remoteNotes = response.getNotes();
- Set<Long> remoteIDs = new HashSet<>();
- // pull remote changes: update or create each remote note
- for (CloudNote remoteNote : remoteNotes) {
- Log.v(TAG, " Process Remote Note: " + remoteNote);
- remoteIDs.add(remoteNote.getRemoteId());
- if (remoteNote.getModified() == null) {
- Log.v(TAG, " ... unchanged");
- } else if (idMap.containsKey(remoteNote.getRemoteId())) {
- Log.v(TAG, " ... found → Update");
- Long remoteId = idMap.get(remoteNote.getRemoteId());
- if (remoteId != null) {
- db.updateNote(localAccount, remoteId, remoteNote, null);
- } else {
- Log.e(TAG, "Tried to update note from server, but remoteId of note is null. " + remoteNote);
- }
- } else {
- Log.v(TAG, " ... create");
- db.addNote(localAccount.getId(), remoteNote);
- }
- }
- Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
- // remove remotely deleted notes (only those without local changes)
- for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
- if (!remoteIDs.contains(entry.getKey())) {
- Log.v(TAG, " ... remove " + entry.getValue());
- db.deleteNote(entry.getValue(), DBStatus.VOID);
- }
- }
-
- // update ETag and Last-Modified in order to reduce size of next response
- localAccount.setETag(response.getETag());
- localAccount.setModified(response.getLastModified());
- db.updateETag(localAccount.getId(), localAccount.getEtag());
- db.updateModified(localAccount.getId(), localAccount.getModified());
- try {
- if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
- localAccount.setPreferredApiVersion(response.getSupportedApiVersions());
- }
- } catch (Exception e) {
- exceptions.add(e);
- }
- return true;
- } catch (NextcloudHttpRequestFailedException e) {
- Log.d(TAG, "Server returned HTTP Status Code " + e.getStatusCode() + " - " + e.getMessage());
- if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
- return true;
- } else {
- exceptions.add(e);
- return false;
- }
- } catch (Exception e) {
- if (e instanceof TokenMismatchException) {
- SSOClient.invalidateAPICache(ssoAccount);
- }
- exceptions.add(e);
- return false;
- }
- }
-
- @Override
- protected void onPostExecute(SyncResultStatus status) {
- super.onPostExecute(status);
- for (Throwable e : exceptions) {
- Log.e(TAG, e.getMessage(), e);
- }
- if (!status.pullSuccessful || !status.pushSuccessful) {
- if (context instanceof ViewProvider && context instanceof AppCompatActivity) {
- BrandedSnackbar.make(((ViewProvider) context).getView(), R.string.error_synchronization, Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(exceptions)
- .show(((AppCompatActivity) context).getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .show();
- }
- }
- syncActive.put(ssoAccount.name, false);
- // notify callbacks
- if (callbacks.containsKey(ssoAccount.name) && callbacks.get(ssoAccount.name) != null) {
- for (ISyncCallback callback : Objects.requireNonNull(callbacks.get(ssoAccount.name))) {
- callback.onFinish();
- }
- }
- db.notifyWidgets();
- db.updateDynamicShortcuts(localAccount.getId());
- // start next sync if scheduled meanwhile
- if (syncScheduled.containsKey(ssoAccount.name) && syncScheduled.get(ssoAccount.name) != null && Boolean.TRUE.equals(syncScheduled.get(ssoAccount.name))) {
- scheduleSync(ssoAccount, false);
- }
- }
- }
-
- public interface ViewProvider {
- View getView();
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java
index cffce6c9..a03705e4 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java
@@ -18,6 +18,7 @@ import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
+import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@@ -25,8 +26,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse;
import it.niedermann.owncloud.notes.shared.model.ServerSettings;
@@ -87,15 +88,24 @@ public abstract class NotesClient {
}
@SuppressWarnings("WeakerAccess")
- public NotesClient(@NonNull Context appContext) {
+ protected NotesClient(@NonNull Context appContext) {
this.appContext = appContext;
}
- abstract NotesResponse getNotes(SingleSignOnAccount ssoAccount, long lastModified, String lastETag) throws Exception;
+ /**
+ * Gets the list of notes from the server.
+ *
+ * @param ssoAccount Account to be used
+ * @param lastModified Last modified time of a former response (Unix timestamp in seconds!). All notes older than this time will be skipped.
+ * @param lastETag ETag of a former response. If nothing changed, the response will be 304 NOT MODIFIED.
+ * @return list of notes
+ * @throws Exception
+ */
+ abstract NotesResponse getNotes(SingleSignOnAccount ssoAccount, Calendar lastModified, String lastETag) throws Exception;
- abstract NoteResponse createNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception;
+ abstract NoteResponse createNote(SingleSignOnAccount ssoAccount, Note note) throws Exception;
- abstract NoteResponse editNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception;
+ abstract NoteResponse editNote(SingleSignOnAccount ssoAccount, Note note) throws Exception;
abstract void deleteNote(SingleSignOnAccount ssoAccount, long noteId) throws Exception;
@@ -114,9 +124,9 @@ public abstract class NotesClient {
private final String content;
private final String etag;
private final String supportedApiVersions;
- private final long lastModified;
+ private final Calendar lastModified;
- ResponseData(@NonNull String content, String etag, long lastModified, @Nullable String supportedApiVersions) {
+ ResponseData(@NonNull String content, String etag, @NonNull Calendar lastModified, @Nullable String supportedApiVersions) {
this.content = content;
this.etag = etag;
this.lastModified = lastModified;
@@ -131,7 +141,7 @@ public abstract class NotesClient {
return etag;
}
- public long getLastModified() {
+ public Calendar getLastModified() {
return lastModified;
}
@@ -175,8 +185,8 @@ public abstract class NotesClient {
try {
Log.v(TAG, ssoAccount.name + " → " + nextcloudRequest.getMethod() + " " + nextcloudRequest.getUrl() + " ");
+ Log.d(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
final Response response = SSOClient.requestFilesApp(appContext, ssoAccount, nextcloudRequest);
- Log.v(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
final BufferedReader rd = new BufferedReader(new InputStreamReader(response.getBody()));
String line;
@@ -191,10 +201,11 @@ public abstract class NotesClient {
etag = Objects.requireNonNull(eTagHeader.getValue()).replace("\"", "");
}
- long lastModified = 0;
+ final Calendar lastModified = Calendar.getInstance();
+ lastModified.setTimeInMillis(0);
final AidlNetworkRequest.PlainHeader lastModifiedHeader = response.getPlainHeader(HEADER_KEY_LAST_MODIFIED);
if (lastModifiedHeader != null)
- lastModified = new Date(lastModifiedHeader.getValue()).getTime() / 1000;
+ lastModified.setTimeInMillis(Date.parse(lastModifiedHeader.getValue()));
Log.d(TAG, "ETag: " + etag + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
String supportedApiVersions = null;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java
index e529dd8c..31a4de39 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java
@@ -9,10 +9,11 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount;
import org.json.JSONObject;
+import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse;
@@ -25,29 +26,32 @@ public class NotesClientV02 extends NotesClient {
super(appContext);
}
- NotesResponse getNotes(SingleSignOnAccount ssoAccount, long lastModified, String lastETag) throws Exception {
- Map<String, String> parameter = new HashMap<>();
- parameter.put(GET_PARAM_KEY_PRUNE_BEFORE, Long.toString(lastModified));
+ NotesResponse getNotes(SingleSignOnAccount ssoAccount, Calendar lastModified, String lastETag) throws Exception {
+ final Map<String, String> parameter = new HashMap<>();
+ parameter.put(GET_PARAM_KEY_PRUNE_BEFORE, Long.toString(lastModified == null ? 0 : lastModified.getTimeInMillis() / 1_000));
return new NotesResponse(requestServer(ssoAccount, "notes", METHOD_GET, parameter, null, lastETag));
}
- private NoteResponse putNote(SingleSignOnAccount ssoAccount, CloudNote note, String path, String method) throws Exception {
+ private NoteResponse putNote(SingleSignOnAccount ssoAccount, Note note, String path, String method) throws Exception {
JSONObject paramObject = new JSONObject();
paramObject.accumulate(JSON_CONTENT, note.getContent());
- paramObject.accumulate(JSON_MODIFIED, note.getModified().getTimeInMillis() / 1000);
- paramObject.accumulate(JSON_FAVORITE, note.isFavorite());
+ paramObject.accumulate(JSON_MODIFIED, note.getModified().getTimeInMillis() / 1_000);
+ paramObject.accumulate(JSON_FAVORITE, note.getFavorite());
paramObject.accumulate(JSON_CATEGORY, note.getCategory());
return new NoteResponse(requestServer(ssoAccount, path, method, null, paramObject, null));
}
- NoteResponse createNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception {
+ @Override
+ NoteResponse createNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
return putNote(ssoAccount, note, "notes", METHOD_POST);
}
- NoteResponse editNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception {
+ @Override
+ NoteResponse editNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
return putNote(ssoAccount, note, "notes/" + note.getRemoteId(), METHOD_PUT);
}
+ @Override
void deleteNote(SingleSignOnAccount ssoAccount, long noteId) throws Exception {
this.requestServer(ssoAccount, "notes/" + noteId, METHOD_DELETE, null, null, null);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java
index c9150f6c..614d99b5 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java
@@ -9,10 +9,11 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount;
import org.json.JSONObject;
+import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse;
import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse;
import it.niedermann.owncloud.notes.shared.model.ServerSettings;
@@ -26,30 +27,33 @@ public class NotesClientV1 extends NotesClient {
super(appContext);
}
- NotesResponse getNotes(SingleSignOnAccount ssoAccount, long lastModified, String lastETag) throws Exception {
- Map<String, String> parameter = new HashMap<>();
- parameter.put(GET_PARAM_KEY_PRUNE_BEFORE, Long.toString(lastModified));
+ NotesResponse getNotes(SingleSignOnAccount ssoAccount, Calendar lastModified, String lastETag) throws Exception {
+ final Map<String, String> parameter = new HashMap<>();
+ parameter.put(GET_PARAM_KEY_PRUNE_BEFORE, Long.toString(lastModified == null ? 0 : lastModified.getTimeInMillis() / 1_000));
return new NotesResponse(requestServer(ssoAccount, "notes", METHOD_GET, parameter, null, lastETag));
}
- private NoteResponse putNote(SingleSignOnAccount ssoAccount, CloudNote note, String path, String method) throws Exception {
+ private NoteResponse putNote(SingleSignOnAccount ssoAccount, Note note, String path, String method) throws Exception {
final JSONObject paramObject = new JSONObject();
paramObject.accumulate(JSON_TITLE, note.getTitle());
paramObject.accumulate(JSON_CONTENT, note.getContent());
- paramObject.accumulate(JSON_MODIFIED, note.getModified().getTimeInMillis() / 1000);
- paramObject.accumulate(JSON_FAVORITE, note.isFavorite());
+ paramObject.accumulate(JSON_MODIFIED, note.getModified().getTimeInMillis() / 1_000);
+ paramObject.accumulate(JSON_FAVORITE, note.getFavorite());
paramObject.accumulate(JSON_CATEGORY, note.getCategory());
return new NoteResponse(requestServer(ssoAccount, path, method, null, paramObject, null));
}
- NoteResponse createNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception {
+ @Override
+ NoteResponse createNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
return putNote(ssoAccount, note, "notes", METHOD_POST);
}
- NoteResponse editNote(SingleSignOnAccount ssoAccount, CloudNote note) throws Exception {
+ @Override
+ NoteResponse editNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
return putNote(ssoAccount, note, "notes/" + note.getRemoteId(), METHOD_PUT);
}
+ @Override
void deleteNote(SingleSignOnAccount ssoAccount, long noteId) throws Exception {
this.requestServer(ssoAccount, "notes/" + noteId, METHOD_DELETE, null, null, null);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
index 78462927..40d918df 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
@@ -1,32 +1,30 @@
package it.niedermann.owncloud.notes.persistence;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteConstraintException;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Color;
import android.graphics.drawable.Icon;
-import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
-import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.TypeConverters;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
import org.json.JSONArray;
import org.json.JSONException;
@@ -34,461 +32,197 @@ import org.json.JSONException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import it.niedermann.android.util.ColorUtil;
+import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
-import it.niedermann.owncloud.notes.main.NavigationAdapter;
+import it.niedermann.owncloud.notes.persistence.dao.AccountDao;
+import it.niedermann.owncloud.notes.persistence.dao.CategoryOptionsDao;
+import it.niedermann.owncloud.notes.persistence.dao.NoteDao;
+import it.niedermann.owncloud.notes.persistence.dao.WidgetNotesListDao;
+import it.niedermann.owncloud.notes.persistence.dao.WidgetSingleNoteDao;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryOptions;
+import it.niedermann.owncloud.notes.persistence.entity.Converters;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_10_11;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_11_12;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_12_13;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_13_14;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_14_15;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_15_16;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_16_17;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_17_18;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_18_19;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_19_20;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_20_21;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
-import it.niedermann.owncloud.notes.shared.model.Category;
import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
-import it.niedermann.owncloud.notes.shared.model.CloudNote;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
-import it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData;
-import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget;
-import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidgetData;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.O;
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets;
-import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_CATEGORY;
import static it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget.updateSingleNoteWidgets;
-
-/**
- * Helps to add, get, update and delete Notes with the option to trigger a Resync with the Server.
- */
-public class NotesDatabase extends AbstractNotesDatabase {
+import static java.util.stream.Collectors.toMap;
+
+@Database(
+ entities = {
+ Account.class,
+ Note.class,
+ CategoryOptions.class,
+ SingleNoteWidgetData.class,
+ NotesListWidgetData.class
+ }, version = 21
+)
+@TypeConverters({Converters.class})
+public abstract class NotesDatabase extends RoomDatabase {
private static final String TAG = NotesDatabase.class.getSimpleName();
-
- private static final String default_order = key_favorite + " DESC, " + key_modified + " DESC";
-
+ private static final String NOTES_DB_NAME = "OWNCLOUD_NOTES";
private static NotesDatabase instance;
+ private static Context context;
+ private static NotesServerSyncHelper serverSyncHelper;
+ private static String defaultNonEmptyTitle;
- private final String defaultNonEmptyTitle;
-
- private final NoteServerSyncHelper serverSyncHelper;
-
- private NotesDatabase(@NonNull Context context) {
- super(context, database_name, null);
- serverSyncHelper = NoteServerSyncHelper.getInstance(this);
+ private static NotesDatabase create(final Context context) {
defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", context);
+ return Room.databaseBuilder(
+ context,
+ NotesDatabase.class,
+ NOTES_DB_NAME)
+ .addMigrations(
+ new Migration_9_10(), // v2.0.0
+ new Migration_10_11(context),
+ new Migration_11_12(context),
+ new Migration_12_13(context),
+ new Migration_13_14(context, () -> instance.notifyWidgets()),
+ new Migration_14_15(),
+ new Migration_15_16(context, () -> instance.notifyWidgets()),
+ new Migration_16_17(),
+ new Migration_17_18(),
+ new Migration_18_19(context),
+ new Migration_19_20(context),
+ new Migration_20_21()
+ )
+ .fallbackToDestructiveMigrationOnDowngrade()
+ .fallbackToDestructiveMigration()
+ .addCallback(new RoomDatabase.Callback() {
+ @Override
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ super.onCreate(db);
+ final String cleanUpStatement = "DELETE FROM CategoryOptions WHERE CategoryOptions.category NOT IN (SELECT Note.category FROM Note WHERE Note.accountId = CategoryOptions.accountId);";
+ db.execSQL("CREATE TRIGGER TRG_CLEANUP_CATEGORIES_DEL AFTER DELETE ON Note BEGIN " + cleanUpStatement + " END;");
+ db.execSQL("CREATE TRIGGER TRG_CLEANUP_CATEGORIES_UPD AFTER UPDATE ON Note BEGIN " + cleanUpStatement + " END;");
+ Log.v(TAG, NotesDatabase.class.getSimpleName() + " created.");
+ }
+ })
+ .allowMainThreadQueries() // FIXME Needed in BaseNoteFragment#saveNote()
+ .build();
}
- public static NotesDatabase getInstance(Context context) {
- if (instance == null)
- return instance = new NotesDatabase(context);
- else
- return instance;
- }
-
- public NoteServerSyncHelper getNoteServerSyncHelper() {
- return serverSyncHelper;
- }
-
- /**
- * Creates a new Note in the Database and adds a Synchronization Flag.
- *
- * @param note Note
- */
- public long addNoteAndSync(SingleSignOnAccount ssoAccount, long accountId, CloudNote note) {
- DBNote dbNote = new DBNote(0, 0, note.getModified(), note.getTitle(), note.getContent(), note.isFavorite(), note.getCategory(), note.getEtag(), DBStatus.LOCAL_EDITED, accountId, generateNoteExcerpt(note.getContent(), note.getTitle()), 0);
- long id = addNote(accountId, dbNote);
- notifyWidgets();
- getNoteServerSyncHelper().scheduleSync(ssoAccount, true);
- return id;
- }
-
- /**
- * Inserts a note directly into the Database.
- * No Synchronisation will be triggered! Use addNoteAndSync()!
- *
- * @param note Note to be added. Remotely created Notes must be of type CloudNote and locally created Notes must be of Type DBNote (with DBStatus.LOCAL_EDITED)!
- */
- long addNote(long accountId, CloudNote note) {
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(11);
- if (note instanceof DBNote) {
- DBNote dbNote = (DBNote) note;
- if (dbNote.getId() > 0) {
- values.put(key_id, dbNote.getId());
- }
- values.put(key_status, dbNote.getStatus().getTitle());
- values.put(key_account_id, dbNote.getAccountId());
- values.put(key_excerpt, dbNote.getExcerpt());
- } else {
- values.put(key_status, DBStatus.VOID.getTitle());
- values.put(key_account_id, accountId);
- values.put(key_excerpt, generateNoteExcerpt(note.getContent(), note.getTitle()));
- }
- if (note.getRemoteId() > 0) {
- values.put(key_remote_id, note.getRemoteId());
- }
- values.put(key_title, note.getTitle());
- values.put(key_modified, note.getModified().getTimeInMillis() / 1000);
- values.put(key_content, note.getContent());
- values.put(key_favorite, note.isFavorite());
- values.put(key_category, getCategoryIdByTitle(accountId, note.getCategory()));
- values.put(key_etag, note.getEtag());
- return db.insert(table_notes, null, values);
- }
-
- public void moveNoteToAnotherAccount(SingleSignOnAccount ssoAccount, long oldAccountId, DBNote note, long newAccountId) {
- // Add new note
- addNoteAndSync(ssoAccount, newAccountId, new CloudNote(0, note.getModified(), note.getTitle(), note.getContent(), note.isFavorite(), note.getCategory(), null));
- deleteNoteAndSync(ssoAccount, note.getId());
-
- notifyWidgets();
- getNoteServerSyncHelper().scheduleSync(ssoAccount, true);
- }
-
- /**
- * Get a single Note by ID
- *
- * @param id int - ID of the requested Note
- * @return requested {@link DBNote}
- */
- public DBNote getNote(long accountId, long id) {
- List<DBNote> notes = getNotesCustom(accountId, key_id + " = ? AND " + key_status + " != ? AND " + key_account_id + " = ? ", new String[]{String.valueOf(id), DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, null, false);
- return notes.isEmpty() ? null : notes.get(0);
- }
-
- /**
- * Gets all the remoteIds of all not deleted notes of an account
- *
- * @param accountId get the remoteIds from all notes of this account
- * @return {@link Set<String>} remoteIds from all notes
- */
- public Set<String> getRemoteIds(long accountId) {
- Cursor cursor = getReadableDatabase()
- .query(
- table_notes,
- new String[]{key_remote_id},
- key_status + " != ? AND " + key_account_id + " = ?",
- new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId},
- null,
- null,
- null
- );
- Set<String> remoteIds = new HashSet<>();
- while (cursor.moveToNext()) {
- remoteIds.add(cursor.getString(0));
- }
- cursor.close();
- return remoteIds;
- }
-
- /**
- * Get a single Note by remote Id (aka. nextcloud file id)
- *
- * @param remoteId int - remote ID of the requested Note
- * @return {@link DBNote#getId()}
- */
- public long getLocalIdByRemoteId(long accountId, long remoteId) {
- List<DBNote> notes = getNotesCustom(accountId, key_remote_id + " = ? AND " + key_status + " != ? AND " + key_account_id + " = ? ", new String[]{String.valueOf(remoteId), DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, null, true);
- if (notes.isEmpty() || notes.get(0) == null) {
- throw new IllegalArgumentException("There is no note with remoteId \"" + remoteId + "\"");
- }
- return notes.get(0).getId();
- }
+ public abstract AccountDao getAccountDao();
- /**
- * Query the database with a custom raw query.
- *
- * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself).
- * @param selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in order that they appear in the selection. The values will be bound as Strings.
- * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.
- * @return {@link List<DBNote>}
- */
- @NonNull
- @WorkerThread
- private List<DBNote> getNotesCustom(long accountId, @NonNull String selection, @NonNull String[] selectionArgs, @Nullable String orderBy, boolean pruneContent) {
- return this.getNotesCustom(accountId, selection, selectionArgs, orderBy, null, pruneContent);
- }
+ public abstract CategoryOptionsDao getCategoryOptionsDao();
- @NonNull
- @WorkerThread
- private List<DBNote> getNotesCustom(long accountId, @NonNull String selection, @NonNull String[] selectionArgs, @Nullable String orderBy, @Nullable String limit, boolean pruneContent) {
- if (selectionArgs.length > 2) {
- Log.v(TAG, selection + " ---- " + selectionArgs[0] + " " + selectionArgs[1] + " " + selectionArgs[2]);
- }
- String cols = String.format("%s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
- key_id, key_remote_id, key_status, key_title, key_modified, key_favorite, key_category_title, key_etag, key_excerpt, key_scroll_y);
- if (!pruneContent) {
- cols = String.format("%s, %s", cols, key_content);
- }
- String rawQuery = "SELECT " + cols + " FROM " + table_notes + " INNER JOIN " + table_category + " ON " + key_category + " = " + key_category_id +
- " WHERE " + selection + (orderBy == null ? "" : " ORDER BY " + orderBy) + (limit == null ? "" : " LIMIT " + limit);
- Cursor cursor = getReadableDatabase().rawQuery(rawQuery, selectionArgs);
+ public abstract NoteDao getNoteDao();
- List<DBNote> notes = new ArrayList<>();
- while (cursor.moveToNext()) {
- notes.add(getNoteFromCursor(accountId, cursor, pruneContent));
- }
- cursor.close();
- return notes;
- }
+ public abstract WidgetSingleNoteDao getWidgetSingleNoteDao();
- /**
- * Creates a DBNote object from the current row of a Cursor.
- *
- * @param cursor database cursor
- * @param pruneContent whether or not the content should be pruned for performance reasons
- * @return {@link DBNote}
- */
- @NonNull
- private DBNote getNoteFromCursor(long accountId, @NonNull Cursor cursor, boolean pruneContent) {
- validateAccountId(accountId);
- Calendar modified = Calendar.getInstance();
- modified.setTimeInMillis(cursor.getLong(4) * 1000);
- return new DBNote(
- cursor.getLong(0),
- cursor.getLong(1),
- modified,
- cursor.getString(3),
- pruneContent ? "" : cursor.getString(10),
- cursor.getInt(5) > 0,
- cursor.getString(6),
- cursor.getString(7),
- DBStatus.parse(cursor.getString(2)),
- accountId,
- cursor.getString(8),
- cursor.getInt(9)
- );
- }
+ public abstract WidgetNotesListDao getWidgetNotesListDao();
- @NonNull
- @WorkerThread
- public Map<Long, Long> getIdMap(long accountId) {
- validateAccountId(accountId);
- Map<Long, Long> result = new HashMap<>();
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(table_notes, new String[]{key_remote_id, key_id}, key_status + " != ? AND " + key_account_id + " = ? ", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, null, null, null);
- while (cursor.moveToNext()) {
- result.put(cursor.getLong(0), cursor.getLong(1));
+ public static NotesDatabase getInstance(@NonNull Context context) {
+ if (instance == null) {
+ instance = create(context.getApplicationContext());
+ NotesDatabase.context = context.getApplicationContext();
+ NotesDatabase.serverSyncHelper = NotesServerSyncHelper.getInstance(instance);
}
- cursor.close();
- return result;
- }
-
- /**
- * Returns a list of all Notes in the Database
- *
- * @return List&lt;Note&gt;
- */
- @NonNull
- @WorkerThread
- public List<DBNote> getNotes(long accountId) {
- validateAccountId(accountId);
- return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, default_order, false);
- }
-
- @NonNull
- @WorkerThread
- public List<DBNote> getRecentNotes(long accountId) {
- validateAccountId(accountId);
- return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, key_modified + " DESC", "4", true);
+ return instance;
}
- /**
- * This method is overloading searchNotes method.
- * In order to keep the original code (called this method) still work.
- *
- * @return {@link List<DBNote>}
- */
- @NonNull
- @WorkerThread
- public List<DBNote> searchNotes(long accountId, @Nullable CharSequence query,
- @Nullable String category, @Nullable Boolean favorite) {
- return searchNotes(accountId, query, category, favorite, null);
+ public NotesServerSyncHelper getNoteServerSyncHelper() {
+ return NotesDatabase.serverSyncHelper;
}
/**
- * Returns a list of all Notes in the Database
- * This method only supports to return the notes in the categories with the matched title or the notes in the categories whose ancestor category matches
- * For example, three categories with the title "abc", "abc/aaa" and "abcd"
- * If search with "abc", then only notes in "abc" and "abc/aaa" will be returned.
+ * Creates a new Note in the Database and adds a Synchronization Flag.
*
- * @return {@link List<DBNote>}
+ * @param note Note
*/
@NonNull
- @WorkerThread
- public List<DBNote> searchNotes(long accountId, @Nullable CharSequence query,
- @Nullable String category, @Nullable Boolean favorite,
- @Nullable CategorySortingMethod sortingMethod) {
- validateAccountId(accountId);
- List<String> where = new ArrayList<>(6);
- List<String> args = new ArrayList<>(9);
-
- where.add(key_status + " != ?");
- args.add(DBStatus.LOCAL_DELETED.getTitle());
-
- where.add(key_account_id + " = ?");
- args.add("" + accountId);
-
- if (query != null) {
- where.add(key_status + " != ?");
- args.add(DBStatus.LOCAL_DELETED.getTitle());
-
- where.add("(" + key_title + " LIKE ? OR " + key_content + " LIKE ? OR " + key_category + " LIKE ?" + ")");
- args.add("%" + query + "%");
- args.add("%" + query + "%");
- args.add("%" + query + "%");
- }
-
- if (category != null) {
- where.add(key_category + " IN (SELECT " + key_category_id + " FROM " + table_category +
- " WHERE " + key_category_title + " =? OR " + key_category_title + " LIKE ?)");
- args.add(category);
- args.add(category + "/%");
- }
-
- if (favorite != null) {
- where.add(key_favorite + "=?");
- args.add(favorite ? "1" : "0");
- }
-
- String order = category == null ? default_order : key_category + ", " + key_title;
- if (sortingMethod != null) {
- if (category != null) { // Needed for subcategories, see https://github.com/stefan-niedermann/nextcloud-notes/issues/902
- order = key_category + "," + key_favorite + " DESC," + sortingMethod.getSorder(); // Edited
- } else {
- order = key_favorite + " DESC," + sortingMethod.getSorder();
- }
- }
- return getNotesCustom(accountId, TextUtils.join(" AND ", where), args.toArray(new String[]{}), order, true);
+ @MainThread
+ public LiveData<Note> addNoteAndSync(Account account, Note note) {
+ final Note entity = new Note(0, null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), note.getETag(), DBStatus.LOCAL_EDITED, account.getId(), generateNoteExcerpt(note.getContent(), note.getTitle()), 0);
+ final MutableLiveData<Note> ret = new MutableLiveData<>();
+ new Thread(() -> ret.postValue(addNote(account.getId(), entity))).start();
+ return map(ret, newNote -> {
+ notifyWidgets();
+ serverSyncHelper.scheduleSync(account, true);
+ return newNote;
+ });
}
/**
- * Returns a list of all Notes in the Database which were modified locally
+ * Inserts a note directly into the Database.
+ * Excerpt will be generated, {@link DBStatus#LOCAL_EDITED} will be applied in case the note has
+ * already has a local ID, otherwise {@link DBStatus#VOID} will be applied.
+ * No Synchronisation will be triggered! Use {@link #addNoteAndSync(Account, Note)}!
*
- * @return {@link List<DBNote>}
+ * @param note {@link Note} to be added.
*/
@NonNull
@WorkerThread
- List<DBNote> getLocalModifiedNotes(long accountId) {
- validateAccountId(accountId);
- return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.VOID.getTitle(), "" + accountId}, null, false);
- }
-
- @NonNull
- @WorkerThread
- public Map<String, Integer> getFavoritesCount(long accountId) {
- validateAccountId(accountId);
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(
- table_notes,
- new String[]{key_favorite, "COUNT(*)"},
- key_status + " != ? AND " + key_account_id + " = ?",
- new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId},
- key_favorite,
- null,
- key_favorite);
- Map<String, Integer> favorites = new HashMap<>(cursor.getCount());
- while (cursor.moveToNext()) {
- favorites.put(cursor.getString(0), cursor.getInt(1));
- }
- cursor.close();
- return favorites;
+ public Note addNote(long accountId, @NonNull Note note) {
+ note.setStatus(note.getId() > 0 ? DBStatus.LOCAL_EDITED : DBStatus.VOID);
+ note.setAccountId(accountId);
+ note.setExcerpt(generateNoteExcerpt(note.getContent(), note.getTitle()));
+ return getNoteDao().getNoteById(getNoteDao().addNote(note));
}
- /**
- * This method return all of the categories with given accountId
- *
- * @param accountId The user account Id
- * @return All of the categories with given accountId
- */
- @NonNull
- @WorkerThread
- public List<NavigationAdapter.CategoryNavigationItem> getCategories(long accountId) {
- return searchCategories(accountId, null);
+ @MainThread
+ public LiveData<Note> moveNoteToAnotherAccount(Account account, @NonNull Note note) {
+ return switchMap(getNoteDao().getContent$(note.getId()), (content) -> {
+ final Note fullNote = new Note(null, note.getModified(), note.getTitle(), content, note.getCategory(), note.getFavorite(), null);
+ deleteNoteAndSync(account, note.getId());
+ return addNoteAndSync(account, fullNote);
+ });
}
/**
- * This method return the category list containing all of the categories containing the
- * search pattern and matched with the given accountId
- * The join operation is used because it is needed that the number of notes in each category
- * If search pattern is null, this method will return all of the categories for corresponding accountId
- *
- * @param accountId The user account ID
- * @param search The search pattern
- * @return The category list containing all of the categories matched
+ * @return a {@link Map} of remote IDs as keys and local IDs as values of all {@link Note}s of
+ * the given {@param accountId} which are not {@link DBStatus#LOCAL_DELETED}
*/
@NonNull
@WorkerThread
- public List<NavigationAdapter.CategoryNavigationItem> searchCategories(long accountId, String search) {
- validateAccountId(accountId);
- String columns = key_category_id + ", " + key_category_title + ", COUNT(*)";
- String selection = key_status + " != ? AND " +
- key_category_account_id + " = ? AND " +
- key_category_title + " LIKE ? " +
- (search == null ? "" : " AND " + key_category_title + " != \"\"");
- String rawQuery = "SELECT " + columns +
- " FROM " + table_category +
- " INNER JOIN " + table_notes +
- " ON " + key_category + " = " + key_category_id +
- " WHERE " + selection +
- " GROUP BY " + key_category_title;
-
- Cursor cursor = getReadableDatabase().rawQuery(rawQuery,
- new String[]{DBStatus.LOCAL_DELETED.getTitle(), String.valueOf(accountId),
- search == null ? "%" : "%" + search.trim() + "%"});
-
- List<NavigationAdapter.CategoryNavigationItem> categories = new ArrayList<>(cursor.getCount());
- while (cursor.moveToNext()) {
- Resources res = getContext().getResources();
- String category = cursor.getString(1).toLowerCase();
- int icon = NavigationAdapter.ICON_FOLDER;
- if (category.equals(res.getString(R.string.category_music).toLowerCase())) {
- icon = R.drawable.ic_library_music_grey600_24dp;
- } else if (category.equals(res.getString(R.string.category_movies).toLowerCase()) || category.equals(res.getString(R.string.category_movie).toLowerCase())) {
- icon = R.drawable.ic_local_movies_grey600_24dp;
- } else if (category.equals(res.getString(R.string.category_work).toLowerCase())) {
- icon = R.drawable.ic_work_grey600_24dp;
- }
- categories.add(new NavigationAdapter.CategoryNavigationItem("category:" + cursor.getString(1), cursor.getString(1), cursor.getInt(2), icon, cursor.getLong(0)));
- }
-
- cursor.close();
- return categories;
- }
-
- public String getCategoryTitleById(long accountId, long categoryId) {
+ public Map<Long, Long> getIdMap(long accountId) {
validateAccountId(accountId);
- final String categoryTitle;
- final Cursor cursor = getReadableDatabase().query(table_category, new String[]{key_category_title}, key_category_id + " = ?", new String[]{String.valueOf(categoryId)}, null, null, null);
- if (cursor.moveToFirst()) {
- categoryTitle = cursor.getString(0);
- } else {
- categoryTitle = null;
- }
- cursor.close();
- return categoryTitle;
+ return getNoteDao()
+ .getRemoteIdAndId(accountId)
+ .stream()
+ .filter(note -> note.getRemoteId() != null)
+ .collect(toMap(Note::getRemoteId, Note::getId));
}
- public void toggleFavorite(SingleSignOnAccount ssoAccount, @NonNull DBNote note, @Nullable ISyncCallback callback) {
- note.setFavorite(!note.isFavorite());
- note.setStatus(DBStatus.LOCAL_EDITED);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(2);
- values.put(key_status, note.getStatus().getTitle());
- values.put(key_favorite, note.isFavorite() ? "1" : "0");
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(note.getId())});
- if (callback != null) {
- serverSyncHelper.addCallbackPush(ssoAccount, callback);
- }
- serverSyncHelper.scheduleSync(ssoAccount, true);
+ @AnyThread
+ public void toggleFavoriteAndSync(Account account, long noteId) {
+ new Thread(() -> {
+ getNoteDao().toggleFavorite(noteId);
+ serverSyncHelper.scheduleSync(account, true);
+ }).start();
}
/**
@@ -496,38 +230,17 @@ public class NotesDatabase extends AbstractNotesDatabase {
* This method will search in the database to find out the category id in the db.
* If there is no such category existing, this method will create it and search again.
*
- * @param ssoAccount The single sign on account
- * @param note The note which will be updated
- * @param category The category title which should be used to find the category id.
- * @param callback When the synchronization is finished, this callback will be invoked (optional).
+ * @param account The single sign on account
+ * @param noteId The note which will be updated
+ * @param category The category title which should be used to find the category id.
*/
- public void setCategory(SingleSignOnAccount ssoAccount, @NonNull DBNote note, @NonNull String category, @Nullable ISyncCallback callback) {
- note.setCategory(category);
- note.setStatus(DBStatus.LOCAL_EDITED);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(2);
- values.put(key_status, note.getStatus().getTitle());
- int id = getCategoryIdByTitle(note.getAccountId(), note.getCategory());
- values.put(key_category, id);
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(note.getId())});
- removeEmptyCategory(note.getAccountId());
- if (callback != null) {
- serverSyncHelper.addCallbackPush(ssoAccount, callback);
- }
- serverSyncHelper.scheduleSync(ssoAccount, true);
- }
-
- private long addCategory(long accountId, @NonNull String title) {
- validateAccountId(accountId);
- SQLiteDatabase db = getWritableDatabase();
- ContentValues values = new ContentValues(2);
- values.put(key_category_account_id, accountId);
- values.put(key_category_title, title);
- return db.insert(table_category, null, values);
- }
-
- public DBNote updateNoteAndSync(SingleSignOnAccount ssoAccount, @NonNull LocalAccount localAccount, @NonNull DBNote oldNote, @Nullable String newContent, @Nullable ISyncCallback callback) {
- return updateNoteAndSync(ssoAccount, localAccount, oldNote, newContent, null, callback);
+ @AnyThread
+ public void setCategory(@NonNull Account account, long noteId, @NonNull String category) {
+ new Thread(() -> {
+ getNoteDao().updateStatus(noteId, DBStatus.LOCAL_EDITED);
+ getNoteDao().updateCategory(noteId, category);
+ serverSyncHelper.scheduleSync(account, true);
+ }).start();
}
/**
@@ -538,44 +251,35 @@ public class NotesDatabase extends AbstractNotesDatabase {
* @param newContent New content. If this is <code>null</code>, then <code>oldNote</code> is saved again (useful for undoing changes).
* @param newTitle New title. If this is <code>null</code>, then either the old title is reused (in case the note has been synced before) or a title is generated (in case it is a new note)
* @param callback When the synchronization is finished, this callback will be invoked (optional).
- * @return changed {@link DBNote} if differs from database, otherwise the old {@link DBNote}.
+ * @return changed {@link Note} if differs from database, otherwise the old {@link Note}.
*/
- public DBNote updateNoteAndSync(SingleSignOnAccount ssoAccount, @NonNull LocalAccount localAccount, @NonNull DBNote oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) {
- DBNote newNote;
+ @WorkerThread
+ public Note updateNoteAndSync(Account localAccount, @NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) {
+ final Note newNote;
if (newContent == null) {
- newNote = new DBNote(oldNote.getId(), oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.isFavorite(), oldNote.getCategory(), oldNote.getEtag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
} else {
final String title;
if (newTitle != null) {
title = newTitle;
} else {
- if ((oldNote.getRemoteId() == 0 || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 0, 0)) < 0) &&
- (defaultNonEmptyTitle.equals(oldNote.getTitle()))) {
- title = NoteUtil.generateNonEmptyNoteTitle(newContent, getContext());
+ if ((oldNote.getRemoteId() == null || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 0, 0)) < 0) &&
+ (defaultNonEmptyTitle.equals(oldNote.getTitle()))) {
+ title = NoteUtil.generateNonEmptyNoteTitle(newContent, context);
} else {
title = oldNote.getTitle();
}
}
- newNote = new DBNote(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), title, newContent, oldNote.isFavorite(), oldNote.getCategory(), oldNote.getEtag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY());
}
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(7);
- values.put(key_status, newNote.getStatus().getTitle());
- values.put(key_title, newNote.getTitle());
- values.put(key_category, getCategoryIdByTitle(newNote.getAccountId(), newNote.getCategory()));
- values.put(key_modified, newNote.getModified().getTimeInMillis() / 1000);
- values.put(key_content, newNote.getContent());
- values.put(key_excerpt, newNote.getExcerpt());
- values.put(key_scroll_y, newNote.getScrollY());
- int rows = db.update(table_notes, values, key_id + " = ? AND (" + key_content + " != ? OR " + key_category + " != ?)", new String[]{String.valueOf(newNote.getId()), newNote.getContent(), newNote.getCategory()});
- removeEmptyCategory(localAccount.getId());
+ int rows = getNoteDao().updateNote(newNote);
// if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly.
if (rows > 0) {
notifyWidgets();
if (callback != null) {
- serverSyncHelper.addCallbackPush(ssoAccount, callback);
+ serverSyncHelper.addCallbackPush(localAccount, callback);
}
- serverSyncHelper.scheduleSync(ssoAccount, true);
+ serverSyncHelper.scheduleSync(localAccount, true);
return newNote;
} else {
if (callback != null) {
@@ -585,135 +289,65 @@ public class NotesDatabase extends AbstractNotesDatabase {
}
}
- public void updateScrollY(long noteId, int scrollY) {
- Log.e(TAG, "Updated scrollY: " + scrollY);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(1);
- values.put(key_scroll_y, scrollY);
- db.update(table_notes, values, key_id + " = ? ", new String[]{String.valueOf(noteId)});
- }
-
- /**
- * Updates a single Note with data from the server, (if it was not modified locally).
- * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization.
- * This is used by the synchronization task, hence no Synchronization will be triggered. Use updateNoteAndSync() instead!
- *
- * @param id local ID of Note
- * @param remoteNote Note from the server.
- * @param forceUnchangedDBNoteState is not null, then the local note is updated only if it was not modified meanwhile
- */
- void updateNote(LocalAccount localAccount, long id, @NonNull CloudNote remoteNote, @Nullable DBNote forceUnchangedDBNoteState) {
- SQLiteDatabase db = this.getWritableDatabase();
-
- // First, update the remote ID, since this field cannot be changed in parallel, but have to be updated always.
- ContentValues values = new ContentValues(8);
- values.put(key_remote_id, remoteNote.getRemoteId());
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(id)});
-
- // The other columns have to be updated in dependency of forceUnchangedDBNoteState,
- // since the Synchronization-Task must not overwrite locales changes!
- values.clear();
- values.put(key_status, DBStatus.VOID.getTitle());
- values.put(key_title, remoteNote.getTitle());
- values.put(key_modified, remoteNote.getModified().getTimeInMillis() / 1000);
- values.put(key_content, remoteNote.getContent());
- values.put(key_favorite, remoteNote.isFavorite());
- values.put(key_category, getCategoryIdByTitle(localAccount.getId(), remoteNote.getCategory()));
- values.put(key_etag, remoteNote.getEtag());
- values.put(key_excerpt, generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
- String whereClause;
- String[] whereArgs;
- if (forceUnchangedDBNoteState != null) {
- // used by: NoteServerSyncHelper.SyncTask.pushLocalChanges()
- // update only, if not modified locally during the synchronization
- // (i.e. all (!) user changeable columns (content, favorite, category) must still have the same value),
- // uses reference value gathered at start of synchronization
- whereClause = key_id + " = ? AND " + key_content + " = ? AND " + key_favorite + " = ? AND " + key_category + " = ?";
- whereArgs = new String[]{String.valueOf(id), forceUnchangedDBNoteState.getContent(), forceUnchangedDBNoteState.isFavorite() ? "1" : "0", String.valueOf(getCategoryIdByTitle(localAccount.getId(), forceUnchangedDBNoteState.getCategory()))};
- } else {
- // used by: NoteServerSyncHelper.SyncTask.pullRemoteChanges()
- // update only, if not modified locally (i.e. STATUS="") and if modified remotely (i.e. any (!) column has changed)
- whereClause = key_id + " = ? AND " + key_status + " = ? AND (" + key_modified + "!=? OR " + key_title + "!=? OR " + key_favorite + "!=? OR " + key_category + "!=? OR " + (remoteNote.getEtag() != null ? key_etag + " IS NULL OR " : "") + key_etag + "!=? OR " + key_content + "!=?)";
- whereArgs = new String[]{String.valueOf(id), DBStatus.VOID.getTitle(), Long.toString(remoteNote.getModified().getTimeInMillis() / 1000), remoteNote.getTitle(), remoteNote.isFavorite() ? "1" : "0", remoteNote.getCategory(), remoteNote.getEtag(), remoteNote.getContent()};
- }
- int i = db.update(table_notes, values, whereClause, whereArgs);
- removeEmptyCategory(id);
- Log.d(TAG, "updateNote: " + remoteNote + " || forceUnchangedDBNoteState: " + forceUnchangedDBNoteState + " => " + i + " rows updated");
- }
-
/**
* Marks a Note in the Database as Deleted. In the next Synchronization it will be deleted
* from the Server.
*
* @param id long - ID of the Note that should be deleted
*/
- public void deleteNoteAndSync(SingleSignOnAccount ssoAccount, long id) {
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(1);
- values.put(key_status, DBStatus.LOCAL_DELETED.getTitle());
- db.update(table_notes,
- values,
- key_id + " = ?",
- new String[]{String.valueOf(id)});
- notifyWidgets();
- getNoteServerSyncHelper().scheduleSync(ssoAccount, true);
+ @AnyThread
+ public void deleteNoteAndSync(Account account, long id) {
+ new Thread(() -> {
+ getNoteDao().updateStatus(id, DBStatus.LOCAL_DELETED);
+ notifyWidgets();
+ serverSyncHelper.scheduleSync(account, true);
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class);
- if (shortcutManager != null) {
- shortcutManager.getPinnedShortcuts().forEach((shortcut) -> {
- String shortcutId = id + "";
- if (shortcut.getId().equals(shortcutId)) {
- Log.v(TAG, "Removing shortcut for " + shortcutId);
- shortcutManager.disableShortcuts(Collections.singletonList(shortcutId), getContext().getResources().getString(R.string.note_has_been_deleted));
- }
- });
- } else {
- Log.e(TAG, ShortcutManager.class.getSimpleName() + "is null.");
+ if (SDK_INT >= O) {
+ ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
+ if (shortcutManager != null) {
+ shortcutManager.getPinnedShortcuts().forEach((shortcut) -> {
+ String shortcutId = id + "";
+ if (shortcut.getId().equals(shortcutId)) {
+ Log.v(TAG, "Removing shortcut for " + shortcutId);
+ shortcutManager.disableShortcuts(Collections.singletonList(shortcutId), context.getResources().getString(R.string.note_has_been_deleted));
+ }
+ });
+ } else {
+ Log.e(TAG, ShortcutManager.class.getSimpleName() + "is null.");
+ }
}
- }
- }
-
- /**
- * Delete a single Note from the Database, if it has a specific DBStatus.
- * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization.
- *
- * @param id long - ID of the Note that should be deleted.
- * @param forceDBStatus DBStatus, e.g., if Note was marked as LOCAL_DELETED (for NoteSQLiteOpenHelper.SyncTask.pushLocalChanges()) or is unchanged VOID (for NoteSQLiteOpenHelper.SyncTask.pullRemoteChanges())
- */
- void deleteNote(long id, @NonNull DBStatus forceDBStatus) {
- SQLiteDatabase db = this.getWritableDatabase();
- db.delete(table_notes,
- key_id + " = ? AND " + key_status + " = ?",
- new String[]{String.valueOf(id), forceDBStatus.getTitle()});
- removeEmptyCategory(id);
+ }).start();
}
/**
* Notify about changed notes.
*/
+ @AnyThread
protected void notifyWidgets() {
- updateSingleNoteWidgets(getContext());
- updateNoteListWidgets(getContext());
+ new Thread(() -> {
+ updateSingleNoteWidgets(context);
+ updateNoteListWidgets(context);
+ }).start();
}
+ @AnyThread
void updateDynamicShortcuts(long accountId) {
new Thread(() -> {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
- ShortcutManager shortcutManager = getContext().getApplicationContext().getSystemService(ShortcutManager.class);
+ if (SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
+ ShortcutManager shortcutManager = context.getApplicationContext().getSystemService(ShortcutManager.class);
if (shortcutManager != null) {
if (!shortcutManager.isRateLimitingActive()) {
List<ShortcutInfo> newShortcuts = new ArrayList<>();
- for (DBNote note : getRecentNotes(accountId)) {
+ for (Note note : getNoteDao().getRecentNotes(accountId)) {
if (!TextUtils.isEmpty(note.getTitle())) {
- Intent intent = new Intent(getContext().getApplicationContext(), EditNoteActivity.class);
+ Intent intent = new Intent(context.getApplicationContext(), EditNoteActivity.class);
intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
intent.setAction(ACTION_SHORTCUT);
- newShortcuts.add(new ShortcutInfo.Builder(getContext().getApplicationContext(), note.getId() + "")
+ newShortcuts.add(new ShortcutInfo.Builder(context.getApplicationContext(), note.getId() + "")
.setShortLabel(note.getTitle() + "")
- .setIcon(Icon.createWithResource(getContext().getApplicationContext(), note.isFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
+ .setIcon(Icon.createWithResource(context.getApplicationContext(), note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
.setIntent(intent)
.build());
} else {
@@ -730,141 +364,9 @@ public class NotesDatabase extends AbstractNotesDatabase {
}).start();
}
- public long getAccountsCount() {
- return DatabaseUtils.queryNumEntries(getReadableDatabase(), table_accounts);
- }
-
- /**
- * @param url URL to the root of the used Nextcloud instance without trailing slash
- * @param username Username of the account
- * @param accountName Composed by the username and the host of the URL, separated by @-sign
- * @param capabilities {@link Capabilities} object containing information about the brand colors, supported API versions, etc...
- * @throws SQLiteConstraintException in case accountName already exists
- */
- public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) throws SQLiteConstraintException {
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(4);
- values.put(key_url, url);
- values.put(key_username, username);
- values.put(key_account_name, accountName);
- values.put(key_capabilities_etag, capabilities.getETag());
- long accountId = db.insertOrThrow(table_accounts, null, values);
- updateBrand(accountId, capabilities);
- }
-
- /**
- * @param accountId account which should be read
- * @return a {@link LocalAccount} object for the given accountId
- */
- public LocalAccount getAccount(long accountId) {
- validateAccountId(accountId);
- final SQLiteDatabase db = getReadableDatabase();
- final Cursor cursor = db.query(table_accounts, new String[]{key_id, key_url, key_account_name, key_username, key_etag, key_modified, key_api_version, key_color, key_text_color, key_capabilities_etag}, key_id + " = ?", new String[]{String.valueOf(accountId)}, null, null, null, null);
- final LocalAccount account = new LocalAccount();
- while (cursor.moveToNext()) {
- account.setId(cursor.getLong(0));
- account.setUrl(cursor.getString(1));
- account.setAccountName(cursor.getString(2));
- account.setUserName(cursor.getString(3));
- account.setETag(cursor.getString(4));
- account.setModified(cursor.getLong(5));
- account.setPreferredApiVersion(cursor.getString(6));
- account.setColor(Color.parseColor('#' + cursor.getString(7)));
- account.setTextColor(Color.parseColor('#' + cursor.getString(8)));
- account.setCapabilitiesETag(cursor.getString(9));
- }
- cursor.close();
- return account;
- }
-
- @NonNull
- public List<LocalAccount> getAccounts() {
- final SQLiteDatabase db = getReadableDatabase();
- final Cursor cursor = db.query(table_accounts, new String[]{key_id, key_url, key_account_name, key_username, key_etag, key_modified, key_api_version, key_color, key_text_color, key_capabilities_etag}, null, null, null, null, null);
- final List<LocalAccount> accounts = new ArrayList<>(cursor.getCount());
- while (cursor.moveToNext()) {
- LocalAccount account = new LocalAccount();
- account.setId(cursor.getLong(0));
- account.setUrl(cursor.getString(1));
- account.setAccountName(cursor.getString(2));
- account.setUserName(cursor.getString(3));
- account.setETag(cursor.getString(4));
- account.setModified(cursor.getLong(5));
- account.setPreferredApiVersion(cursor.getString(6));
- account.setColor(Color.parseColor('#' + cursor.getString(7)));
- account.setTextColor(Color.parseColor('#' + cursor.getString(8)));
- account.setCapabilitiesETag(cursor.getString(9));
- accounts.add(account);
- }
- cursor.close();
- return accounts;
- }
-
- @Nullable
- public LocalAccount getLocalAccountByAccountName(String accountName) throws IllegalArgumentException {
- if (accountName == null) {
- Log.e(TAG, "accountName is null");
- return null;
- }
- final SQLiteDatabase db = getReadableDatabase();
- final Cursor cursor = db.query(table_accounts, new String[]{key_id, key_url, key_account_name, key_username, key_etag, key_modified, key_api_version, key_color, key_text_color, key_capabilities_etag}, key_account_name + " = ?", new String[]{accountName}, null, null, null, null);
- final LocalAccount account = new LocalAccount();
- int numberEntries = 0;
- while (cursor.moveToNext()) {
- numberEntries++;
- account.setId(cursor.getLong(0));
- account.setUrl(cursor.getString(1));
- account.setAccountName(cursor.getString(2));
- account.setUserName(cursor.getString(3));
- account.setETag(cursor.getString(4));
- account.setModified(cursor.getLong(5));
- account.setPreferredApiVersion(cursor.getString(6));
- account.setColor(Color.parseColor('#' + cursor.getString(7)));
- account.setTextColor(Color.parseColor('#' + cursor.getString(8)));
- account.setCapabilitiesETag(cursor.getString(9));
- }
- cursor.close();
- switch (numberEntries) {
- case 0:
- Log.w(TAG, "Could not find any account for \"" + accountName + "\". Returning null.");
- return null;
- case 1:
- return account;
- default:
- Log.e(TAG, "", new IllegalArgumentException("Expected to find 1 account for name \"" + accountName + "\", but found " + numberEntries + "."));
- return null;
- }
- }
-
- public void updateBrand(long accountId, @NonNull Capabilities capabilities) throws IllegalArgumentException {
- validateAccountId(accountId);
-
- String color;
- try {
- color = ColorUtil.INSTANCE.formatColorToParsableHexString(capabilities.getColor()).substring(1);
- } catch (Exception e) {
- color = String.format("%06X", (0xFFFFFF & ContextCompat.getColor(context, R.color.defaultBrand)));
- }
-
- String textColor;
- try {
- textColor = ColorUtil.INSTANCE.formatColorToParsableHexString(capabilities.getTextColor()).substring(1);
- } catch (Exception e) {
- textColor = String.format("%06X", (0xFFFFFF & ContextCompat.getColor(context, android.R.color.white)));
- }
-
- final SQLiteDatabase db = this.getWritableDatabase();
- final ContentValues values = new ContentValues(2);
-
- values.put(key_color, color);
- values.put(key_text_color, textColor);
-
- final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{String.valueOf(accountId)});
- if (updatedRows == 1) {
- Log.v(TAG, "Updated " + key_color + " to " + capabilities.getColor() + " and " + key_text_color + " to " + capabilities.getTextColor() + " for " + key_account_id + " = " + accountId);
- } else {
- Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and " + key_color + " = " + capabilities.getColor() + " and " + key_text_color + " = " + capabilities.getTextColor());
- }
+ @AnyThread
+ public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
+ return getAccountDao().getAccountById$(getAccountDao().insert(new Account(url, username, accountName, capabilities)));
}
/**
@@ -881,12 +383,9 @@ public class NotesDatabase extends AbstractNotesDatabase {
ApiVersion.of(apiVersions.getString(i));
}
if (apiVersions.length() > 0) {
- final SQLiteDatabase db = this.getWritableDatabase();
- final ContentValues values = new ContentValues(1);
- values.put(key_api_version, apiVersion);
- final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{String.valueOf(accountId)});
+ final int updatedRows = getAccountDao().updateApiVersion(accountId, apiVersion);
if (updatedRows == 1) {
- Log.i(TAG, "Updated " + key_api_version + " to \"" + apiVersion + "\" for accountId = " + accountId);
+ Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId);
} else {
Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
}
@@ -906,147 +405,35 @@ public class NotesDatabase extends AbstractNotesDatabase {
}
/**
- * @param localAccount the {@link LocalAccount} that should be deleted
+ * @param localAccount the {@link Account} that should be deleted
* @throws IllegalArgumentException if no account has been deleted by the given accountId
*/
- public void deleteAccount(@NonNull LocalAccount localAccount) throws IllegalArgumentException {
+ @AnyThread
+ public LiveData<Void> deleteAccount(@NonNull Account localAccount) throws IllegalArgumentException {
validateAccountId(localAccount.getId());
- SQLiteDatabase db = this.getWritableDatabase();
- int deletedAccounts = db.delete(table_accounts, key_id + " = ?", new String[]{String.valueOf(localAccount.getId())});
- if (deletedAccounts < 1) {
- Log.e(TAG, "AccountId '" + localAccount.getId() + "' did not delete any account");
- throw new IllegalArgumentException("The given accountId does not delete any row");
- } else if (deletedAccounts > 1) {
- Log.e(TAG, "AccountId '" + localAccount.getId() + "' deleted unexpectedly '" + deletedAccounts + "' accounts");
- }
-
- try {
- SSOClient.invalidateAPICache(AccountImporter.getSingleSignOnAccount(getContext(), localAccount.getAccountName()));
- } catch (NextcloudFilesAppAccountNotFoundException e) {
- e.printStackTrace();
- SSOClient.invalidateAPICache();
- }
-
- final int deletedNotes = db.delete(table_notes, key_account_id + " = ?", new String[]{String.valueOf(localAccount.getId())});
- Log.v(TAG, "Deleted " + deletedNotes + " notes from account " + localAccount.getId());
- }
-
- void updateETag(long accountId, String etag) {
- validateAccountId(accountId);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(1);
- values.put(key_etag, etag);
- final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{String.valueOf(accountId)});
- if (updatedRows == 1) {
- Log.v(TAG, "Updated etag to " + etag + " for accountId = " + accountId);
- } else {
- Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and etag = " + etag);
- }
- }
-
- public void updateCapabilitiesETag(long accountId, String capabilitiesETag) {
- validateAccountId(accountId);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(1);
- values.put(key_capabilities_etag, capabilitiesETag);
- final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{String.valueOf(accountId)});
- if (updatedRows == 1) {
- Log.v(TAG, "Updated etag to " + capabilitiesETag + " for accountId = " + accountId);
- } else {
- Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and capabilitiesETag = " + capabilitiesETag);
- }
- }
-
- void updateModified(long accountId, long modified) {
- validateAccountId(accountId);
- if (modified < 0) {
- throw new IllegalArgumentException("modified must be greater or equal 0");
- }
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues(1);
- values.put(key_modified, modified);
- final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{String.valueOf(accountId)});
- if (updatedRows == 1) {
- Log.v(TAG, "Updated modified to " + modified + " for accountId = " + accountId);
- } else {
- Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and modified = " + modified);
- }
- }
-
- /**
- * @param appWidgetId the id of the {@link SingleNoteWidget}
- * @return {@link SingleNoteWidgetData}
- * @throws NoSuchElementException in case there is no {@link SingleNoteWidgetData} for the given appWidgetId
- */
- @NonNull
- public SingleNoteWidgetData getSingleNoteWidgetData(int appWidgetId) throws NoSuchElementException {
- SingleNoteWidgetData data = new SingleNoteWidgetData();
- final SQLiteDatabase db = getReadableDatabase();
- final Cursor cursor = db.query(table_widget_single_notes, new String[]{key_account_id, key_note_id, key_theme_mode}, key_id + " = ?", new String[]{String.valueOf(appWidgetId)}, null, null, null, null);
- if (cursor.moveToNext()) {
- data.setAppWidgetId(appWidgetId);
- data.setAccountId(cursor.getLong(0));
- data.setNoteId(cursor.getLong(1));
- data.setThemeMode(cursor.getInt(2));
- } else {
- throw new NoSuchElementException();
- }
- cursor.close();
- return data;
- }
-
- public void removeSingleNoteWidget(int appWidgetId) {
- final SQLiteDatabase db = getWritableDatabase();
- db.delete(table_widget_single_notes, key_id + " = ?", new String[]{String.valueOf(appWidgetId)});
- }
-
- public void createOrUpdateSingleNoteWidgetData(@NonNull SingleNoteWidgetData data) throws SQLException {
- validateAccountId(data.getAccountId());
- final SQLiteDatabase db = getWritableDatabase();
- final ContentValues values = new ContentValues(4);
- values.put(key_id, data.getAppWidgetId());
- values.put(key_account_id, data.getAccountId());
- values.put(key_note_id, data.getNoteId());
- values.put(key_theme_mode, data.getThemeMode());
- db.replaceOrThrow(table_widget_single_notes, null, values);
- }
-
- @NonNull
- public NoteListsWidgetData getNoteListWidgetData(int appWidgetId) throws NoSuchElementException {
- NoteListsWidgetData data = new NoteListsWidgetData();
- final SQLiteDatabase db = getReadableDatabase();
- final Cursor cursor = db.query(table_widget_note_list, new String[]{key_account_id, key_category_id, key_theme_mode, key_mode}, key_id + " = ?", new String[]{String.valueOf(appWidgetId)}, null, null, null, null);
- if (cursor.moveToNext()) {
- data.setAppWidgetId(appWidgetId);
- data.setAccountId(cursor.getLong(0));
- data.setCategoryId(cursor.getLong(1));
- data.setThemeMode(cursor.getInt(2));
- data.setMode(cursor.getInt(3));
- } else {
- throw new NoSuchElementException();
- }
- cursor.close();
- return data;
- }
+ MutableLiveData<Void> ret = new MutableLiveData<>();
+ new Thread(() -> {
+ int deletedAccounts = getAccountDao().deleteAccount(localAccount);
+ if (deletedAccounts < 1) {
+ Log.e(TAG, "AccountId '" + localAccount.getId() + "' did not delete any account");
+ throw new IllegalArgumentException("The given accountId does not delete any row");
+ } else if (deletedAccounts > 1) {
+ Log.e(TAG, "AccountId '" + localAccount.getId() + "' deleted unexpectedly '" + deletedAccounts + "' accounts");
+ }
- public void removeNoteListWidget(int appWidgetId) {
- final SQLiteDatabase db = getWritableDatabase();
- db.delete(table_widget_note_list, key_id + " = ?", new String[]{String.valueOf(appWidgetId)});
- }
+ try {
+ SSOClient.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName()));
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ e.printStackTrace();
+ SSOClient.invalidateAPICache();
+ }
- public void createOrUpdateNoteListWidgetData(@NonNull NoteListsWidgetData data) throws SQLException {
- validateAccountId(data.getAccountId());
- final SQLiteDatabase db = getWritableDatabase();
- final ContentValues values = new ContentValues(5);
- if (data.getMode() != MODE_DISPLAY_CATEGORY && data.getCategoryId() != null) {
- throw new UnsupportedOperationException("Cannot create a widget with a categoryId when mode is not " + MODE_DISPLAY_CATEGORY);
- }
- values.put(key_id, data.getAppWidgetId());
- values.put(key_account_id, data.getAccountId());
- values.put(key_category_id, data.getCategoryId());
- values.put(key_theme_mode, data.getThemeMode());
- values.put(key_mode, data.getMode());
- db.replaceOrThrow(table_widget_note_list, null, values);
+ // TODO this should already be handled by foreign key cascade, no?
+ final int deletedNotes = getNoteDao().deleteByAccountId(localAccount.getId());
+ Log.v(TAG, "Deleted " + deletedNotes + " notes from account " + localAccount.getId());
+ ret.postValue(null);
+ }).start();
+ return ret;
}
private static void validateAccountId(long accountId) {
@@ -1056,191 +443,108 @@ public class NotesDatabase extends AbstractNotesDatabase {
}
/**
- * Get the category if with the given category title
- * The method does not support fuzzy search.
- * Because the category title in database is unique, there will not at most one result.
- * If there is no such category, database will create it if create flag is set.
- * Otherwise this method will return -1 as default value.
- *
- * @param accountId The user {@link LocalAccount} Id
- * @param categoryTitle The category title which will be search in the db
- * @return -1 if there is no such category else the corresponding id
- */
- @NonNull
- @WorkerThread
- private Integer getCategoryIdByTitle(long accountId, @NonNull String categoryTitle) {
- validateAccountId(accountId);
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(
- table_category,
- new String[]{key_category_id},
- key_category_title + " = ? AND " + key_category_account_id + " = ? ",
- new String[]{categoryTitle, String.valueOf(accountId)},
- null,
- null,
- null);
- int id;
- if (cursor.moveToNext()) {
- id = cursor.getInt(0);
- } else {
- id = (int) addCategory(accountId, categoryTitle);
- if (id == -1) {
- Log.e(TAG, String.format("Error occurs when creating category: %s", categoryTitle));
- }
- }
- cursor.close();
- return id;
- }
-
- /**
- * This function will be called when the category or note is updated.
- * Because sometime we will remove some notes in categories.
- * Such that there must be such a category without any note.
- * For these useless category, it is better to remove.
- * Move a note from a category to another may also lead to the same result.
- *
- * @param accountId The user accountId
- */
- private void removeEmptyCategory(long accountId) {
- validateAccountId(accountId);
- getReadableDatabase().delete(table_category,
- key_category_id + " NOT IN (SELECT " + key_category + " FROM " + table_notes + ")",
- null);
- }
-
- /**
- * This function is used to get the sorting method of a category by title.
- * The sorting method of the category can be used to decide
- * to use which sorting method to show the notes for each categories.
- *
- * @param accountId The user accountID
- * @param categoryTitle The category title
- * @return The sorting method in {@link CategorySortingMethod} enum format
- */
- public CategorySortingMethod getCategoryOrderByTitle(long accountId, String categoryTitle) {
- validateAccountId(accountId);
-
- long categoryId = getCategoryIdByTitle(accountId, categoryTitle);
-
- SQLiteDatabase db = getReadableDatabase();
- int orderIndex;
- try (Cursor cursor = db.query(table_category, new String[]{key_category_sorting_method},
- key_category_id + " = ?", new String[]{String.valueOf(categoryId)},
- null, null, null)) {
- orderIndex = 0;
- while (cursor.moveToNext()) {
- orderIndex = cursor.getInt(0);
- }
- }
-
- return CategorySortingMethod.getCSM(orderIndex);
- }
-
- /**
- * This method is used to modify the sorting method for one category by title.
+ * Modifies the sorting method for one category, the category can be normal category or
+ * one of "All notes", "Favorite", and "Uncategorized".
+ * If category is one of these three, sorting method will be modified in android.content.SharedPreference.
* The user can determine use which sorting method to show the notes for a category.
* When the user changes the sorting method, this method should be called.
*
- * @param accountId The user accountID
- * @param categoryTitle The category title
- * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
+ * @param accountId The user accountID
+ * @param selectedCategory The category to be modified
+ * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
*/
- public void modifyCategoryOrderByTitle(
- long accountId, String categoryTitle, CategorySortingMethod sortingMethod) {
+ @AnyThread
+ public void modifyCategoryOrder(long accountId, @NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
validateAccountId(accountId);
- long categoryId = getCategoryIdByTitle(accountId, categoryTitle);
-
- SQLiteDatabase db = getWritableDatabase();
-
- ContentValues values = new ContentValues();
- values.put(key_category_sorting_method, sortingMethod.getCSMID());
- db.update(table_category, values,
- key_category_id + " = ?", new String[]{String.valueOf(categoryId)});
+ new Thread(() -> {
+ final Context ctx = context.getApplicationContext();
+ final SharedPreferences.Editor sp = PreferenceManager.getDefaultSharedPreferences(ctx).edit();
+ int orderIndex = sortingMethod.getId();
+
+ switch (selectedCategory.getType()) {
+ case FAVORITES: {
+ sp.putInt(ctx.getString(R.string.action_sorting_method) + ' ' + ctx.getString(R.string.label_favorites), orderIndex);
+ break;
+ }
+ case UNCATEGORIZED: {
+ sp.putInt(ctx.getString(R.string.action_sorting_method) + ' ' + ctx.getString(R.string.action_uncategorized), orderIndex);
+ break;
+ }
+ case RECENT: {
+ sp.putInt(ctx.getString(R.string.action_sorting_method) + ' ' + ctx.getString(R.string.label_all_notes), orderIndex);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category != null) {
+ if (getCategoryOptionsDao().modifyCategoryOrder(accountId, category, sortingMethod) == 0) {
+ // Nothing updated means we didn't have this yet
+ final CategoryOptions categoryOptions = new CategoryOptions();
+ categoryOptions.setAccountId(accountId);
+ categoryOptions.setCategory(category);
+ categoryOptions.setSortingMethod(sortingMethod);
+ getCategoryOptionsDao().addCategoryOptions(categoryOptions);
+ }
+ } else {
+ throw new IllegalStateException("Tried to modify category order for " + ENavigationCategoryType.DEFAULT_CATEGORY + "but category is null.");
+ }
+ break;
+ }
+ }
+ sp.apply();
+ }).start();
}
/**
- * Gets the sorting method of a category, the category can be normal category or
- * one of "All notes", "Favorite", and "Uncategorized".
- * If category is one of these three, sorting method will be got from android.content.SharedPreference.
- * The sorting method of the category can be used to decide
- * to use which sorting method to show the notes for each categories.
+ * Gets the sorting method of a {@link NavigationCategory}, the category can be normal
+ * {@link CategoryOptions} or one of {@link ENavigationCategoryType}.
+ * If the category no normal {@link CategoryOptions}, sorting method will be got from
+ * {@link SharedPreferences}.
+ * <p>
+ * The sorting method of the category can be used to decide to use which sorting method to show
+ * the notes for each categories.
*
- * @param accountId The user accountID
- * @param category The category
+ * @param selectedCategory The category
* @return The sorting method in CategorySortingMethod enum format
*/
- public CategorySortingMethod getCategoryOrder(long accountId, Category category) {
- validateAccountId(accountId);
-
- final Context ctx = getContext().getApplicationContext();
- final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(ctx);
- int orderIndex;
-
- if (category.category == null) {
- if (category.favorite != null && category.favorite) {
- // Favorite
- orderIndex = sp.getInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.label_favorites),
- 0);
- } else {
- // All notes
- orderIndex = sp.getInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.label_all_notes),
- 0);
+ @NonNull
+ @MainThread
+ public LiveData<CategorySortingMethod> getCategoryOrder(@NonNull NavigationCategory selectedCategory) {
+ final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+ String prefKey;
+
+ switch (selectedCategory.getType()) {
+ // TODO make this account specific
+ case RECENT: {
+ prefKey = context.getString(R.string.action_sorting_method) + ' ' + context.getString(R.string.label_all_notes);
+ break;
+ }
+ case FAVORITES: {
+ prefKey = context.getString(R.string.action_sorting_method) + ' ' + context.getString(R.string.label_favorites);
+ break;
+ }
+ case UNCATEGORIZED: {
+ prefKey = context.getString(R.string.action_sorting_method) + ' ' + context.getString(R.string.action_uncategorized);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category != null) {
+ return getCategoryOptionsDao().getCategoryOrder(selectedCategory.getAccountId(), category);
+ } else {
+ Log.e(TAG, "Cannot read " + CategorySortingMethod.class.getSimpleName() + " for " + ENavigationCategoryType.DEFAULT_CATEGORY + ".");
+ return new MutableLiveData<>(CategorySortingMethod.SORT_MODIFIED_DESC);
+ }
}
- } else if (category.category.isEmpty()) {
- // Uncategorized
- orderIndex = sp.getInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.action_uncategorized),
- 0);
- } else {
- return getCategoryOrderByTitle(accountId, category.category);
}
- return CategorySortingMethod.getCSM(orderIndex);
+ return map(new SharedPreferenceIntLiveData(sp, prefKey, CategorySortingMethod.SORT_MODIFIED_DESC.getId()), CategorySortingMethod::findById);
}
- /**
- * Modifies the sorting method for one category, the category can be normal category or
- * one of "All notes", "Favorite", and "Uncategorized".
- * If category is one of these three, sorting method will be modified in android.content.SharedPreference.
- * The user can determine use which sorting method to show the notes for a category.
- * When the user changes the sorting method, this method should be called.
- *
- * @param accountId The user accountID
- * @param category The category to be modified
- * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
- */
- public void modifyCategoryOrder(
- long accountId, Category category, CategorySortingMethod sortingMethod) {
- validateAccountId(accountId);
-
- final Context ctx = getContext().getApplicationContext();
- final SharedPreferences.Editor sp = PreferenceManager.getDefaultSharedPreferences(ctx).edit();
- int orderIndex = sortingMethod.getCSMID();
- if (category.category == null) {
- if (category.favorite != null && category.favorite) {
- // Favorite
- sp.putInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.label_favorites),
- orderIndex);
- } else {
- // All notes
- sp.putInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.label_all_notes),
- orderIndex);
- }
- } else if (category.category.isEmpty()) {
- // Uncategorized
- sp.putInt(ctx.getString(R.string.action_sorting_method) +
- ' ' + ctx.getString(R.string.action_uncategorized),
- orderIndex);
- } else {
- modifyCategoryOrderByTitle(accountId, category.category, sortingMethod);
- return;
- }
- sp.apply();
+ public Context getContext() {
+ return NotesDatabase.context;
}
-
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java
new file mode 100644
index 00000000..e9fab1b0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java
@@ -0,0 +1,346 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.accounts.NetworkErrorException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.preference.PreferenceManager;
+
+import com.nextcloud.android.sso.AccountImporter;
+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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+import it.niedermann.owncloud.notes.shared.util.SSOUtil;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+/**
+ * Helps to synchronize the Database to the Server.
+ */
+public class NotesServerSyncHelper {
+
+ private static final String TAG = NotesServerSyncHelper.class.getSimpleName();
+
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ private static NotesServerSyncHelper instance;
+
+ private final NotesDatabase db;
+ private final Context context;
+
+ /**
+ * Track network connection changes using a {@link BroadcastReceiver}
+ */
+ private boolean isSyncPossible = false;
+ private boolean networkConnected = false;
+ private String syncOnlyOnWifiKey;
+ private boolean syncOnlyOnWifi;
+ private final MutableLiveData<Boolean> syncStatus = new MutableLiveData<>(false);
+ private final MutableLiveData<ArrayList<Throwable>> syncErrors = new MutableLiveData<>();
+
+ /**
+ * @see <a href="https://stackoverflow.com/a/3104265">Do not make this a local variable.</a>
+ */
+ @SuppressWarnings("FieldCanBeLocal")
+ private final SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = (SharedPreferences prefs, String key) -> {
+ if (syncOnlyOnWifiKey.equals(key)) {
+ syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
+ updateNetworkStatus();
+ }
+ };
+
+ private final BroadcastReceiver networkReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateNetworkStatus();
+ if (isSyncPossible() && SSOUtil.isConfigured(context)) {
+ new Thread(() -> {
+ try {
+ scheduleSync(db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(context).name), false);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ Log.v(TAG, "Can not select current SingleSignOn account after network changed, do not sync.");
+ }
+ }).start();
+ }
+ }
+ };
+
+ // current state of the synchronization
+ private final Map<Long, Boolean> syncActive = new HashMap<>();
+ private final Map<Long, Boolean> syncScheduled = new HashMap<>();
+
+ // list of callbacks for both parts of synchronization
+ private final Map<Long, List<ISyncCallback>> callbacksPush = new HashMap<>();
+ private final Map<Long, List<ISyncCallback>> callbacksPull = new HashMap<>();
+
+ private NotesServerSyncHelper(NotesDatabase db) {
+ this.db = db;
+ this.context = db.getContext();
+ this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
+
+ // Registers BroadcastReceiver to track network connection changes.
+ context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
+ prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
+ syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
+
+ updateNetworkStatus();
+ }
+
+ /**
+ * Get (or create) instance from NoteServerSyncHelper.
+ * This has to be a singleton in order to realize correct registering and unregistering of
+ * the BroadcastReceiver, which listens on changes of network connectivity.
+ *
+ * @param db {@link NotesDatabase}
+ * @return NoteServerSyncHelper
+ */
+ public static synchronized NotesServerSyncHelper getInstance(NotesDatabase db) {
+ if (instance == null) {
+ instance = new NotesServerSyncHelper(db);
+ }
+ return instance;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ context.getApplicationContext().unregisterReceiver(networkReceiver);
+ super.finalize();
+ }
+
+ /**
+ * Synchronization is only possible, if there is an active network connection.
+ * <p>
+ * This method respects the user preference "Sync on Wi-Fi only".
+ * <p>
+ * NoteServerSyncHelper observes changes in the network connection.
+ * The current state can be retrieved with this method.
+ *
+ * @return true if sync is possible, otherwise false.
+ */
+ public boolean isSyncPossible() {
+ return isSyncPossible;
+ }
+
+ public boolean isNetworkConnected() {
+ return networkConnected;
+ }
+
+ public boolean isSyncOnlyOnWifi() {
+ return syncOnlyOnWifi;
+ }
+
+ /**
+ * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server.
+ * All callbacks will be executed once the synchronization operations are done.
+ * After execution the callback will be deleted, so it has to be added again if it shall be
+ * executed the next time all synchronize operations are finished.
+ *
+ * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
+ */
+ public void addCallbackPush(Account account, ISyncCallback callback) {
+ if (account == null) {
+ Log.i(TAG, "ssoAccount is null. Is this a local account?");
+ callback.onScheduled();
+ callback.onFinish();
+ } else {
+ if (!callbacksPush.containsKey(account.getId())) {
+ callbacksPush.put(account.getId(), new ArrayList<>());
+ }
+ Objects.requireNonNull(callbacksPush.get(account.getId())).add(callback);
+ }
+ }
+
+ /**
+ * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server.
+ * All callbacks will be executed once the synchronization operations are done.
+ * After execution the callback will be deleted, so it has to be added again if it shall be
+ * executed the next time all synchronize operations are finished.
+ *
+ * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
+ */
+ public void addCallbackPull(Account account, ISyncCallback callback) {
+ if (account == null) {
+ Log.i(TAG, "ssoAccount is null. Is this a local account?");
+ callback.onScheduled();
+ callback.onFinish();
+ } else {
+ if (!callbacksPull.containsKey(account.getId())) {
+ callbacksPull.put(account.getId(), new ArrayList<>());
+ }
+ Objects.requireNonNull(callbacksPull.get(account.getId())).add(callback);
+ }
+ }
+
+ /**
+ * Schedules a synchronization and start it directly, if the network is connected and no
+ * synchronization is currently running.
+ *
+ * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
+ */
+ public void scheduleSync(Account account, boolean onlyLocalChanges) {
+ if (account == null) {
+ Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
+ } else {
+ if (syncActive.get(account.getId()) == null) {
+ syncActive.put(account.getId(), false);
+ }
+ Log.d(TAG, "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (Boolean.TRUE.equals(syncActive.get(account.getId())) ? "sync active" : "sync NOT active") + ") ...");
+ if (isSyncPossible() && (!Boolean.TRUE.equals(syncActive.get(account.getId())) || onlyLocalChanges)) {
+ try {
+ SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(context, account.getAccountName());
+ Log.d(TAG, "... starting now");
+ final NotesClient notesClient = NotesClient.newInstance(account.getPreferredApiVersion(), context);
+ final NotesServerSyncTask syncTask = new NotesServerSyncTask(notesClient, db, account, ssoAccount, onlyLocalChanges) {
+ @Override
+ void onPreExecute() {
+ syncStatus.postValue(true);
+ if (!syncScheduled.containsKey(localAccount.getId()) || syncScheduled.get(localAccount.getId()) == null) {
+ syncScheduled.put(localAccount.getId(), false);
+ }
+ if (!onlyLocalChanges && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
+ syncScheduled.put(localAccount.getId(), false);
+ }
+ syncActive.put(localAccount.getId(), true);
+ }
+
+ @Override
+ void onPostExecute(SyncResultStatus status) {
+ for (Throwable e : exceptions) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ if (!status.pullSuccessful || !status.pushSuccessful) {
+ syncErrors.postValue(exceptions);
+ }
+ syncActive.put(localAccount.getId(), false);
+ // notify callbacks
+ if (callbacks.containsKey(localAccount.getId()) && callbacks.get(localAccount.getId()) != null) {
+ for (ISyncCallback callback : Objects.requireNonNull(callbacks.get(localAccount.getId()))) {
+ callback.onFinish();
+ }
+ }
+ db.notifyWidgets();
+ db.updateDynamicShortcuts(localAccount.getId());
+ // start next sync if scheduled meanwhile
+ if (syncScheduled.containsKey(localAccount.getId()) && syncScheduled.get(localAccount.getId()) != null && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
+ scheduleSync(localAccount, false);
+ }
+ syncStatus.postValue(false);
+ }
+ };
+ syncTask.addCallbacks(account, callbacksPush.get(account.getId()));
+ callbacksPush.put(account.getId(), new ArrayList<>());
+ if (!onlyLocalChanges) {
+ syncTask.addCallbacks(account, callbacksPull.get(account.getId()));
+ callbacksPull.put(account.getId(), new ArrayList<>());
+ }
+ executor.submit(syncTask);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ Log.e(TAG, "... Could not find " + SingleSignOnAccount.class.getSimpleName() + " for account name " + account.getAccountName());
+ e.printStackTrace();
+ }
+ } else if (!onlyLocalChanges) {
+ Log.d(TAG, "... scheduled");
+ syncScheduled.put(account.getId(), true);
+ if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
+ final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
+ if (callbacks != null) {
+ for (ISyncCallback callback : callbacks) {
+ callback.onScheduled();
+ }
+ } else {
+ Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
+ }
+ }
+ } else {
+ Log.d(TAG, "... do nothing");
+ if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
+ final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
+ if (callbacks != null) {
+ for (ISyncCallback callback : callbacks) {
+ callback.onScheduled();
+ }
+ } else {
+ Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
+ }
+ }
+ }
+ }
+ }
+
+ public void updateNetworkStatus() {
+ try {
+ final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ if (connMgr == null) {
+ throw new NetworkErrorException("ConnectivityManager is null");
+ }
+
+ final NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
+
+ if (activeInfo == null) {
+ throw new NetworkErrorException("NetworkInfo is null");
+ }
+
+ if (activeInfo.isConnected()) {
+ networkConnected = true;
+
+ final NetworkInfo networkInfo = connMgr.getNetworkInfo((ConnectivityManager.TYPE_WIFI));
+
+ if (networkInfo == null) {
+ throw new NetworkErrorException("connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI) is null");
+ }
+
+ isSyncPossible = !syncOnlyOnWifi || networkInfo.isConnected();
+
+ if (isSyncPossible) {
+ Log.d(TAG, "Network connection established.");
+ } else {
+ Log.d(TAG, "Network connected, but not used because only synced on wifi.");
+ }
+ } else {
+ networkConnected = false;
+ isSyncPossible = false;
+ Log.d(TAG, "No network connection.");
+ }
+ } catch (NetworkErrorException e) {
+ e.printStackTrace();
+ networkConnected = false;
+ isSyncPossible = false;
+ }
+ }
+
+ @NonNull
+ public LiveData<Boolean> getSyncStatus() {
+ return distinctUntilChanged(this.syncStatus);
+ }
+
+ @NonNull
+ public LiveData<ArrayList<Throwable>> getSyncErrors() {
+ return this.syncErrors;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
new file mode 100644
index 00000000..ddc05981
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
@@ -0,0 +1,233 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.TokenMismatchException;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.model.ServerResponse;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
+import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+
+
+/**
+ * {@link NotesServerSyncTask} is a {@link Thread} which performs the synchronization in a background thread.
+ * Synchronization consists of two parts: {@link #pushLocalChanges()} and {@link #pullRemoteChanges}.
+ */
+abstract class NotesServerSyncTask extends Thread {
+
+ private static final String TAG = NotesServerSyncTask.class.getSimpleName();
+
+ @NonNull
+ private final NotesClient notesClient;
+ @NonNull
+ private final NotesDatabase db;
+ @NonNull
+ protected final Account localAccount;
+ @NonNull
+ private final SingleSignOnAccount ssoAccount;
+ private final boolean onlyLocalChanges;
+ @NonNull
+ protected final Map<Long, List<ISyncCallback>> callbacks = new HashMap<>();
+ @NonNull
+ protected final ArrayList<Throwable> exceptions = new ArrayList<>();
+
+ NotesServerSyncTask(@NonNull NotesClient notesClient, @NonNull NotesDatabase db, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
+ super(TAG);
+ this.notesClient = notesClient;
+ this.db = db;
+ this.localAccount = localAccount;
+ this.ssoAccount = ssoAccount;
+ this.onlyLocalChanges = onlyLocalChanges;
+ }
+
+ void addCallbacks(Account account, List<ISyncCallback> callbacks) {
+ this.callbacks.put(account.getId(), callbacks);
+ }
+
+ @Override
+ public void run() {
+ onPreExecute();
+
+ Log.i(TAG, "STARTING SYNCHRONIZATION");
+ final SyncResultStatus status = new SyncResultStatus();
+ status.pushSuccessful = pushLocalChanges();
+ if (!onlyLocalChanges) {
+ status.pullSuccessful = pullRemoteChanges();
+ }
+ Log.i(TAG, "SYNCHRONIZATION FINISHED");
+
+ onPostExecute(status);
+ }
+
+ abstract void onPreExecute();
+
+ abstract void onPostExecute(SyncResultStatus status);
+
+ /**
+ * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server.
+ */
+ private boolean pushLocalChanges() {
+ Log.d(TAG, "pushLocalChanges()");
+
+ boolean success = true;
+ final List<Note> notes = db.getNoteDao().getLocalModifiedNotes(localAccount.getId());
+ for (Note note : notes) {
+ Log.d(TAG, " Process Local Note: " + note);
+ try {
+ Note remoteNote;
+ switch (note.getStatus()) {
+ case LOCAL_EDITED:
+ Log.v(TAG, " ...create/edit");
+ if (note.getRemoteId() != null) {
+ Log.v(TAG, " ...Note has remoteId → try to edit");
+ try {
+ remoteNote = notesClient.editNote(ssoAccount, note).getNote();
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_FOUND) {
+ Log.v(TAG, " ...Note does no longer exist on server → recreate");
+ remoteNote = notesClient.createNote(ssoAccount, note).getNote();
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ Log.v(TAG, " ...Note does not have a remoteId yet → create");
+ remoteNote = notesClient.createNote(ssoAccount, note).getNote();
+ db.getNoteDao().updateRemoteId(note.getId(), remoteNote.getRemoteId());
+ }
+ // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
+ db.getNoteDao().updateIfNotModifiedLocallyDuringSync(note.getId(), remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()), note.getContent(), note.getCategory(), note.getFavorite());
+ break;
+ case LOCAL_DELETED:
+ if (note.getRemoteId() == null) {
+ Log.v(TAG, " ...delete (only local, since it has never been synchronized)");
+ } else {
+ Log.v(TAG, " ...delete (from server and local)");
+ try {
+ notesClient.deleteNote(ssoAccount, note.getRemoteId());
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_FOUND) {
+ Log.v(TAG, " ...delete (note has already been deleted remotely)");
+ } else {
+ throw e;
+ }
+ }
+ }
+ // Please note, that db.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
+ db.getNoteDao().deleteByNoteId(note.getId(), LOCAL_DELETED);
+ break;
+ default:
+ throw new IllegalStateException("Unknown State of Note " + note + ": " + note.getStatus());
+ }
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
+ Log.d(TAG, "Server returned HTTP Status Code 304 - Not Modified");
+ } else {
+ exceptions.add(e);
+ success = false;
+ }
+ } catch (Exception e) {
+ if (e instanceof TokenMismatchException) {
+ SSOClient.invalidateAPICache(ssoAccount);
+ }
+ exceptions.add(e);
+ success = false;
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes.
+ */
+ private boolean pullRemoteChanges() {
+ Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
+ try {
+ final Map<Long, Long> idMap = db.getIdMap(localAccount.getId());
+
+ // FIXME re-reading the localAccount is only a workaround for a not-up-to-date eTag in localAccount.
+ final Account accountFromDatabase = db.getAccountDao().getAccountById(localAccount.getId());
+ localAccount.setModified(accountFromDatabase.getModified());
+ localAccount.setETag(accountFromDatabase.getETag());
+
+ final ServerResponse.NotesResponse response = notesClient.getNotes(ssoAccount, localAccount.getModified(), localAccount.getETag());
+ final List<Note> remoteNotes = response.getNotes();
+ final Set<Long> remoteIDs = new HashSet<>();
+ // pull remote changes: update or create each remote note
+ for (Note remoteNote : remoteNotes) {
+ Log.v(TAG, " Process Remote Note: " + remoteNote);
+ remoteIDs.add(remoteNote.getRemoteId());
+ if (remoteNote.getModified() == null) {
+ Log.v(TAG, " ... unchanged");
+ } else if (idMap.containsKey(remoteNote.getRemoteId())) {
+ Log.v(TAG, " ... found → Update");
+ Long localId = idMap.get(remoteNote.getRemoteId());
+ if (localId != null) {
+ db.getNoteDao().updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
+ localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
+ } else {
+ Log.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote);
+ }
+ } else {
+ Log.v(TAG, " ... create");
+ db.addNote(localAccount.getId(), remoteNote);
+ }
+ }
+ Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
+ // remove remotely deleted notes (only those without local changes)
+ for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
+ if (!remoteIDs.contains(entry.getKey())) {
+ Log.v(TAG, " ... remove " + entry.getValue());
+ db.getNoteDao().deleteByNoteId(entry.getValue(), DBStatus.VOID);
+ }
+ }
+
+ // update ETag and Last-Modified in order to reduce size of next response
+ localAccount.setETag(response.getETag());
+ localAccount.setModified(response.getLastModified());
+ db.getAccountDao().updateETag(localAccount.getId(), localAccount.getETag());
+ db.getAccountDao().updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
+ try {
+ if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
+ localAccount.setApiVersion(response.getSupportedApiVersions());
+ }
+ } catch (Exception e) {
+ exceptions.add(e);
+ }
+ return true;
+ } catch (NextcloudHttpRequestFailedException e) {
+ Log.d(TAG, "Server returned HTTP Status Code " + e.getStatusCode() + " - " + e.getMessage());
+ if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
+ return true;
+ } else {
+ exceptions.add(e);
+ return false;
+ }
+ } catch (Exception e) {
+ if (e instanceof TokenMismatchException) {
+ SSOClient.invalidateAPICache(ssoAccount);
+ }
+ exceptions.add(e);
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java
index 4a29bd6d..1b86e0aa 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java
@@ -12,15 +12,11 @@ import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
-import com.nextcloud.android.sso.AccountImporter;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class SyncWorker extends Worker {
@@ -39,15 +35,10 @@ public class SyncWorker extends Worker {
@Override
public Result doWork() {
NotesDatabase db = NotesDatabase.getInstance(getApplicationContext());
- for (LocalAccount account : db.getAccounts()) {
- try {
- SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName());
- Log.v(TAG, "Starting background synchronization for " + ssoAccount.name);
- db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, () -> Log.v(TAG, "Finished background synchronization for " + ssoAccount.name));
- db.getNoteServerSyncHelper().scheduleSync(ssoAccount, false);
- } catch (NextcloudFilesAppAccountNotFoundException e) {
- e.printStackTrace();
- }
+ for (Account account : db.getAccountDao().getAccounts()) {
+ Log.v(TAG, "Starting background synchronization for " + account.getAccountName());
+ db.getNoteServerSyncHelper().addCallbackPull(account, () -> Log.v(TAG, "Finished background synchronization for " + account.getAccountName()));
+ db.getNoteServerSyncHelper().scheduleSync(account, false);
}
// TODO return result depending on callbackPull
return Result.success();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
new file mode 100644
index 00000000..b0828717
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
@@ -0,0 +1,58 @@
+package it.niedermann.owncloud.notes.persistence.dao;
+
+import androidx.annotation.ColorInt;
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+@Dao
+public interface AccountDao {
+
+ @Insert
+ long insert(Account localAccount);
+
+ @Delete
+ int deleteAccount(Account localAccount);
+
+ String getAccounts = "SELECT * FROM Account";
+ String getAccountById = "SELECT * FROM Account WHERE ID = :accountId";
+
+ @Query(getAccounts)
+ LiveData<List<Account>> getAccounts$();
+
+ @Query(getAccounts)
+ List<Account> getAccounts();
+
+ @Query(getAccountById)
+ LiveData<Account> getAccountById$(long accountId);
+
+ @Query(getAccountById)
+ Account getAccountById(long accountId);
+
+ @Query("SELECT * FROM Account WHERE ACCOUNTNAME = :accountName")
+ Account getAccountByName(String accountName);
+
+ @Query("SELECT COUNT(*) FROM Account")
+ LiveData<Integer> countAccounts$();
+
+ @Query("UPDATE Account SET COLOR = :color, TEXTCOLOR = :textColor WHERE id = :id")
+ void updateBrand(long id, @ColorInt Integer color, @ColorInt Integer textColor);
+
+ @Query("UPDATE Account SET ETAG = :eTag WHERE ID = :id")
+ void updateETag(long id, String eTag);
+
+ @Query("UPDATE Account SET CAPABILITIESETAG = :capabilitiesETag WHERE id = :id")
+ void updateCapabilitiesETag(long id, String capabilitiesETag);
+
+ @Query("UPDATE Account SET MODIFIED = :modified WHERE id = :id")
+ void updateModified(long id, long modified);
+
+ @Query("UPDATE Account SET APIVERSION = :apiVersion WHERE id = :id")
+ int updateApiVersion(Long id, String apiVersion);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CategoryOptionsDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CategoryOptionsDao.java
new file mode 100644
index 00000000..46fdb0f5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CategoryOptionsDao.java
@@ -0,0 +1,42 @@
+package it.niedermann.owncloud.notes.persistence.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.persistence.entity.CategoryOptions;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+
+@Dao
+public interface CategoryOptionsDao {
+
+ @Insert
+ void addCategoryOptions(CategoryOptions entity);
+
+ /**
+ * This method is used to modify the sorting method for one category by title.
+ * The user can determine use which sorting method to show the notes for a category.
+ * When the user changes the sorting method, this method should be called.
+ *
+ * @param accountId The user accountID
+ * @param category The category
+ * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
+ */
+ @Query("UPDATE CategoryOptions SET sortingMethod = :sortingMethod WHERE category = :category AND accountId = :accountId")
+ int modifyCategoryOrder(long accountId, String category, CategorySortingMethod sortingMethod);
+
+ /**
+ * This function is used to get the sorting method of a category by title.
+ * The sorting method of the category can be used to decide
+ * to use which sorting method to show the notes for each categories.
+ *
+ * @param category The category
+ * @return The sorting method in {@link CategorySortingMethod} enum format
+ */
+ @Query("SELECT sortingMethod FROM CategoryOptions WHERE accountId = :accountId AND category = :category")
+ LiveData<CategorySortingMethod> getCategoryOrder(long accountId, String category);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
new file mode 100644
index 00000000..b0e93d2c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
@@ -0,0 +1,197 @@
+package it.niedermann.owncloud.notes.persistence.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import java.util.List;
+import java.util.Set;
+
+import it.niedermann.owncloud.notes.persistence.NotesServerSyncHelper;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+
+/**
+ * Each method starting with <code>search</code> will return only a partial {@link Note} without any
+ * {@link Note#eTag}, {@link Note#status}, {@link Note#content} or {@link Note#scrollY} for performance reasons.
+ */
+@SuppressWarnings("JavadocReference")
+@Dao
+public interface NoteDao {
+
+ @Insert
+ long addNote(Note note);
+
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ int updateNote(Note newNote);
+
+ @Query("DELETE FROM NOTE WHERE accountId = :accountId")
+ int deleteByAccountId(Long accountId);
+
+ String getNoteById = "SELECT * FROM NOTE WHERE id = :id";
+ String getContent = "SELECT content FROM NOTE WHERE id = :id";
+ String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId";
+ String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1";
+ String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
+ String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE NOCASE ASC";
+ String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC";
+ String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE NOCASE ASC";
+ String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC";
+ String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE NOCASE ASC";
+ String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC";
+ String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE NOCASE ASC";
+
+ @Query(getNoteById)
+ LiveData<Note> getNoteById$(long id);
+
+ @Query(getNoteById)
+ Note getNoteById(long id);
+
+ @Query(count)
+ LiveData<Integer> count$(long accountId);
+
+ @Query(count)
+ Integer count(long accountId);
+
+ @Query(countFavorites)
+ LiveData<Integer> countFavorites$(long accountId);
+
+ @Query(countFavorites)
+ Integer countFavorites(long accountId);
+
+ @Query(getContent)
+ LiveData<String> getContent$(Long id);
+
+ @Query(getContent)
+ String getContent(Long id);
+
+ @Query(searchRecentByModified)
+ LiveData<List<Note>> searchRecentByModified$(long accountId, String query);
+
+ @Query(searchRecentByModified)
+ List<Note> searchRecentByModified(long accountId, String query);
+
+ @Query(searchRecentLexicographically)
+ LiveData<List<Note>> searchRecentLexicographically$(long accountId, String query);
+
+ @Query(searchRecentLexicographically)
+ List<Note> searchRecentLexicographically(long accountId, String query);
+
+ @Query(searchFavoritesByModified)
+ LiveData<List<Note>> searchFavoritesByModified$(long accountId, String query);
+
+ @Query(searchFavoritesByModified)
+ List<Note> searchFavoritesByModified(long accountId, String query);
+
+ @Query(searchFavoritesLexicographically)
+ LiveData<List<Note>> searchFavoritesLexicographically$(long accountId, String query);
+
+ @Query(searchFavoritesLexicographically)
+ List<Note> searchFavoritesLexicographically(long accountId, String query);
+
+ @Query(searchUncategorizedByModified)
+ LiveData<List<Note>> searchUncategorizedByModified$(long accountId, String query);
+
+ @Query(searchUncategorizedByModified)
+ List<Note> searchUncategorizedByModified(long accountId, String query);
+
+ @Query(searchUncategorizedLexicographically)
+ LiveData<List<Note>> searchUncategorizedLexicographically$(long accountId, String query);
+
+ @Query(searchUncategorizedLexicographically)
+ List<Note> searchUncategorizedLexicographically(long accountId, String query);
+
+ @Query(searchCategoryByModified)
+ LiveData<List<Note>> searchCategoryByModified$(long accountId, String query, String category);
+
+ @Query(searchCategoryByModified)
+ List<Note> searchCategoryByModified(long accountId, String query, String category);
+
+ @Query(searchCategoryLexicographically)
+ LiveData<List<Note>> searchCategoryLexicographically$(long accountId, String query, String category);
+
+ @Query(searchCategoryLexicographically)
+ List<Note> searchCategoryLexicographically(long accountId, String query, String category);
+
+ @Query("DELETE FROM NOTE WHERE id = :id AND status = :forceDBStatus")
+ void deleteByNoteId(long id, DBStatus forceDBStatus);
+
+ @Query("UPDATE NOTE SET scrollY = :scrollY WHERE id = :id")
+ void updateScrollY(long id, int scrollY);
+
+ @Query("UPDATE NOTE SET status = :status WHERE id = :id")
+ void updateStatus(long id, DBStatus status);
+
+ @Query("UPDATE NOTE SET category = :category WHERE id = :id")
+ void updateCategory(long id, String category);
+
+ /**
+ * Gets all the {@link Note#remoteId}s of all not deleted {@link Note}s of an {@link Account}
+ *
+ * @param accountId get the {@link Note#remoteId} from all {@link Note}s of this {@link Account}
+ * @return {@link Set<String>} {@link Note#remoteId}s from all {@link Note}s
+ */
+ @Query("SELECT DISTINCT remoteId FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED'")
+ List<Long> getRemoteIds(long accountId);
+
+ @Query("SELECT id, remoteId, 0 as accountId, '' as title, 0 as favorite, '' as excerpt, 0 as modified, '' as eTag, 0 as status, '' as category, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED'")
+ List<Note> getRemoteIdAndId(long accountId);
+
+ /**
+ * Get a single {@link Note} by {@link Note#remoteId} (aka. Nextcloud file id)
+ *
+ * @param remoteId int - {@link Note#remoteId} of the requested {@link Note}
+ * @return {@link Note#id}
+ */
+ @Query("SELECT id FROM NOTE WHERE accountId = :accountId AND remoteId = :remoteId AND status != 'LOCAL_DELETED'")
+ Long getLocalIdByRemoteId(long accountId, long remoteId);
+
+ /**
+ * Returns a list of all {@link Note}s in the Database which were modified locally
+ *
+ * @return {@link List<Note>}
+ */
+ @Query("SELECT * FROM NOTE WHERE status != '' AND accountId = :accountId")
+ List<Note> getLocalModifiedNotes(long accountId);
+
+ @Query("SELECT * FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId ORDER BY modified DESC LIMIT 4")
+ List<Note> getRecentNotes(long accountId);
+
+ @Query("UPDATE NOTE SET status = 'LOCAL_EDITED', favorite = ((favorite | 1) - (favorite & 1)) WHERE id = :id")
+ void toggleFavorite(long id);
+
+ @Query("UPDATE NOTE SET remoteId = :remoteId WHERE id = :id")
+ void updateRemoteId(long id, Long remoteId);
+
+ /**
+ * used by: {@link NotesServerSyncHelper.SyncTask#pushLocalChanges()} update only, if not modified locally during the synchronization
+ * (i.e. all (!) user changeable columns (content, favorite, category) must still have the same value), uses reference value gathered at start of synchronization
+ */
+ @Query("UPDATE NOTE SET title = :targetTitle, modified = :targetModified, favorite = :targetFavorite, etag = :targetETag, content = :targetContent, status = '', excerpt = :targetExcerpt " +
+ "WHERE id = :noteId AND content = :contentBeforeSyncStart AND favorite = :favoriteBeforeSyncStart AND category = :categoryBeforeSyncStart")
+ int updateIfNotModifiedLocallyDuringSync(long noteId, Long targetModified, String targetTitle, boolean targetFavorite, String targetETag, String targetContent, String targetExcerpt, String contentBeforeSyncStart, String categoryBeforeSyncStart, boolean favoriteBeforeSyncStart);
+
+ /**
+ * used by: {@link NotesServerSyncHelper.SyncTask#pullRemoteChanges()} update only, if not modified locally (i.e. STATUS="") and if modified remotely (i.e. any (!) column has changed)
+ */
+ @Query("UPDATE NOTE SET title = :title, modified = :modified, favorite = :favorite, etag = :eTag, content = :content, status = '', excerpt = :excerpt " +
+ "WHERE id = :id AND status = '' AND (title != :title OR modified != :modified OR favorite != :favorite OR category != :category OR (eTag IS NULL OR eTag != :eTag) OR content != :content)")
+ int updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(long id, Long modified, String title, boolean favorite, String category, String eTag, String content, String excerpt);
+
+ /**
+ * This method return all of the categories with given {@param accountId}
+ *
+ * @param accountId The user account Id
+ * @return All of the categories with given accountId
+ */
+ @Query("SELECT accountId, category, COUNT(*) as 'totalNotes' FROM NOTE WHERE STATUS != 'LOCAL_DELETED' AND accountId = :accountId GROUP BY category")
+ LiveData<List<CategoryWithNotesCount>> getCategories$(Long accountId);
+
+ @Query("SELECT accountId, category, COUNT(*) as 'totalNotes' FROM NOTE WHERE STATUS != 'LOCAL_DELETED' AND accountId = :accountId AND category != '' AND category LIKE :searchTerm GROUP BY category")
+ LiveData<List<CategoryWithNotesCount>> searchCategories$(Long accountId, String searchTerm);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java
new file mode 100644
index 00000000..c198ab2f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java
@@ -0,0 +1,20 @@
+package it.niedermann.owncloud.notes.persistence.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+
+@Dao
+public interface WidgetNotesListDao {
+
+ @Insert
+ void createOrUpdateNoteListWidgetData(NotesListWidgetData data);
+
+ @Query("DELETE FROM NOTESLISTWIDGETDATA WHERE id = :appWidgetId")
+ void removeNoteListWidget(int appWidgetId);
+
+ @Query("SELECT * FROM NOTESLISTWIDGETDATA WHERE id = :appWidgetId")
+ NotesListWidgetData getNoteListWidgetData(int appWidgetId);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetSingleNoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetSingleNoteDao.java
new file mode 100644
index 00000000..dd0d1a19
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetSingleNoteDao.java
@@ -0,0 +1,21 @@
+package it.niedermann.owncloud.notes.persistence.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
+
+@Dao
+public interface WidgetSingleNoteDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void createOrUpdateSingleNoteWidgetData(SingleNoteWidgetData data);
+
+ @Query("DELETE FROM SINGLENOTEWIDGETDATA WHERE id = :id")
+ void removeSingleNoteWidget(int id);
+
+ @Query("SELECT * FROM SINGLENOTEWIDGETDATA WHERE id = :id")
+ SingleNoteWidgetData getSingleNoteWidgetData(int id);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
new file mode 100644
index 00000000..ed6dcd6c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
@@ -0,0 +1,247 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.Serializable;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.NoSuchElementException;
+
+import it.niedermann.owncloud.notes.persistence.NotesClient;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+
+@Entity(
+ indices = {
+ @Index(name = "IDX_ACCOUNT_MODIFIED", value = "modified"),
+ @Index(name = "IDX_ACCOUNT_URL", value = "url"),
+ @Index(name = "IDX_ACCOUNT_USERNAME", value = "userName"),
+ @Index(name = "IDX_ACCOUNT_ACCOUNTNAME", value = "accountName"),
+ @Index(name = "IDX_ACCOUNT_ETAG", value = "eTag")
+ }
+)
+public class Account implements Serializable {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String url = "";
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String userName = "";
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String accountName = "";
+ @Nullable
+ private String eTag;
+ @Nullable
+ private Calendar modified;
+ @Nullable
+ private String apiVersion;
+ @ColorInt
+ @ColumnInfo(defaultValue = "-16743735")
+ private int color = Color.parseColor("#0082C9");
+ @ColorInt
+ @ColumnInfo(defaultValue = "-16777216")
+ private int textColor = Color.WHITE;
+ @Nullable
+ private String capabilitiesETag;
+
+ public Account() {
+ // Default constructor
+ }
+
+ public Account(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
+ setUrl(url);
+ setUserName(username);
+ setAccountName(accountName);
+ setCapabilities(capabilities);
+ }
+
+ @Nullable
+ public ApiVersion getPreferredApiVersion() {
+ // TODO move this logic to NotesClient?
+ try {
+ if (apiVersion == null) {
+ return null;
+ }
+ final JSONArray versionsArray = new JSONArray(apiVersion);
+ final Collection<ApiVersion> supportedApiVersions = new HashSet<>(versionsArray.length());
+ for (int i = 0; i < versionsArray.length(); i++) {
+ final ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i));
+ for (ApiVersion temp : NotesClient.SUPPORTED_API_VERSIONS) {
+ if (temp.compareTo(parsedApiVersion) == 0) {
+ supportedApiVersions.add(parsedApiVersion);
+ break;
+ }
+ }
+ }
+ return Collections.max(supportedApiVersions);
+ } catch (JSONException | NoSuchElementException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void setCapabilities(@NonNull Capabilities capabilities) {
+ capabilitiesETag = capabilities.getETag();
+ apiVersion = capabilities.getApiVersion();
+ setColor(capabilities.getColor());
+ setTextColor(capabilities.getTextColor());
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(@NonNull String url) {
+ this.url = url;
+ }
+
+ @NonNull
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(@NonNull String userName) {
+ this.userName = userName;
+ }
+
+ @NonNull
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public void setAccountName(@NonNull String accountName) {
+ this.accountName = accountName;
+ }
+
+ @Nullable
+ public String getETag() {
+ return eTag;
+ }
+
+ public void setETag(@Nullable String eTag) {
+ this.eTag = eTag;
+ }
+
+ @Nullable
+ public Calendar getModified() {
+ return modified;
+ }
+
+ public void setModified(@Nullable Calendar modified) {
+ this.modified = modified;
+ }
+
+ @Nullable
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public void setApiVersion(@Nullable String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ }
+
+ public int getTextColor() {
+ return textColor;
+ }
+
+ public void setTextColor(int textColor) {
+ this.textColor = textColor;
+ }
+
+ @Nullable
+ public String getCapabilitiesETag() {
+ return capabilitiesETag;
+ }
+
+ public void setCapabilitiesETag(@Nullable String capabilitiesETag) {
+ this.capabilitiesETag = capabilitiesETag;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Account)) return false;
+
+ Account account = (Account) o;
+
+ if (id != account.id) return false;
+ if (color != account.color) return false;
+ if (textColor != account.textColor) return false;
+ if (!url.equals(account.url)) return false;
+ if (!userName.equals(account.userName)) return false;
+ if (!accountName.equals(account.accountName)) return false;
+ if (eTag != null ? !eTag.equals(account.eTag) : account.eTag != null) return false;
+ if (modified != null ? !modified.equals(account.modified) : account.modified != null)
+ return false;
+ if (apiVersion != null ? !apiVersion.equals(account.apiVersion) : account.apiVersion != null)
+ return false;
+ if (capabilitiesETag != null ? !capabilitiesETag.equals(account.capabilitiesETag) : account.capabilitiesETag != null)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (id ^ (id >>> 32));
+ result = 31 * result + url.hashCode();
+ result = 31 * result + userName.hashCode();
+ result = 31 * result + accountName.hashCode();
+ result = 31 * result + (eTag != null ? eTag.hashCode() : 0);
+ result = 31 * result + (modified != null ? modified.hashCode() : 0);
+ result = 31 * result + (apiVersion != null ? apiVersion.hashCode() : 0);
+ result = 31 * result + color;
+ result = 31 * result + textColor;
+ result = 31 * result + (capabilitiesETag != null ? capabilitiesETag.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Account{" +
+ "id=" + id +
+ ", url='" + url + '\'' +
+ ", userName='" + userName + '\'' +
+ ", accountName='" + accountName + '\'' +
+ ", eTag='" + eTag + '\'' +
+ ", modified=" + modified +
+ ", apiVersion='" + apiVersion + '\'' +
+ ", color=" + color +
+ ", textColor=" + textColor +
+ ", capabilitiesETag='" + capabilitiesETag + '\'' +
+ '}';
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryOptions.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryOptions.java
new file mode 100644
index 00000000..da37ee31
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryOptions.java
@@ -0,0 +1,93 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+
+import java.io.Serializable;
+
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+
+@Entity(
+ primaryKeys = {
+ "accountId",
+ "category"
+ },
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId",
+ onDelete = ForeignKey.CASCADE
+ ),
+// Not possible with SQLite because parent column is not unique
+// @ForeignKey(
+// entity = Note.class,
+// parentColumns = {"accountId", "category"},
+// childColumns = {"accountId", "category"},
+// onDelete = ForeignKey.CASCADE
+// )
+ },
+ indices = {
+ @Index(name = "IDX_CATEGORIYOPTIONS_ACCOUNTID", value = "accountId"),
+ @Index(name = "IDX_CATEGORIYOPTIONS_CATEGORY", value = "category"),
+ @Index(name = "IDX_CATEGORIYOPTIONS_SORTING_METHOD", value = "sortingMethod"),
+ @Index(name = "IDX_UNIQUE_CATEGORYOPTIONS_ACCOUNT_CATEGORY", value = {"accountId", "category"}, unique = true)
+ }
+)
+public class CategoryOptions implements Serializable {
+ private long accountId;
+ @NonNull
+ private String category = "";
+ @Nullable
+ private CategorySortingMethod sortingMethod;
+
+ public long getAccountId() {
+ return accountId;
+ }
+
+ public void setAccountId(long accountId) {
+ this.accountId = accountId;
+ }
+
+ @NonNull
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(@NonNull String category) {
+ this.category = category;
+ }
+
+ @Nullable
+ public CategorySortingMethod getSortingMethod() {
+ return sortingMethod;
+ }
+
+ public void setSortingMethod(@Nullable CategorySortingMethod sortingMethod) {
+ this.sortingMethod = sortingMethod;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CategoryOptions)) return false;
+
+ CategoryOptions that = (CategoryOptions) o;
+
+ if (accountId != that.accountId) return false;
+ if (!category.equals(that.category)) return false;
+ return sortingMethod == that.sortingMethod;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (accountId ^ (accountId >>> 32));
+ result = 31 * result + category.hashCode();
+ result = 31 * result + (sortingMethod != null ? sortingMethod.hashCode() : 0);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryWithNotesCount.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryWithNotesCount.java
new file mode 100644
index 00000000..ecdeae58
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/CategoryWithNotesCount.java
@@ -0,0 +1,53 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+public class CategoryWithNotesCount {
+
+ private long accountId;
+ private String category;
+ private Integer totalNotes;
+
+ public Integer getTotalNotes() {
+ return totalNotes;
+ }
+
+ public void setTotalNotes(Integer totalNotes) {
+ this.totalNotes = totalNotes;
+ }
+
+ public long getAccountId() {
+ return accountId;
+ }
+
+ public void setAccountId(long accountId) {
+ this.accountId = accountId;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CategoryWithNotesCount)) return false;
+
+ CategoryWithNotesCount that = (CategoryWithNotesCount) o;
+
+ if (accountId != that.accountId) return false;
+ if (category != null ? !category.equals(that.category) : that.category != null)
+ return false;
+ return totalNotes != null ? totalNotes.equals(that.totalNotes) : that.totalNotes == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (accountId ^ (accountId >>> 32));
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (totalNotes != null ? totalNotes.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Converters.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Converters.java
new file mode 100644
index 00000000..099fec4e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Converters.java
@@ -0,0 +1,57 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.TypeConverter;
+
+import java.util.Calendar;
+
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+
+public class Converters {
+
+ @TypeConverter
+ public static DBStatus fromString(@Nullable String value) {
+ for (DBStatus status : DBStatus.values()) {
+ if (status.getTitle().equals(value)) {
+ return status;
+ }
+ }
+ return DBStatus.VOID;
+ }
+
+ @TypeConverter
+ public static String dbStatusToString(@Nullable DBStatus status) {
+ return status == null ? null : status.getTitle();
+ }
+
+ @TypeConverter
+ @NonNull
+ public static CategorySortingMethod categorySortingMethodFromString(@Nullable Integer value) {
+ return value == null ? CategorySortingMethod.SORT_MODIFIED_DESC : CategorySortingMethod.findById(value);
+ }
+
+ @TypeConverter
+ @Nullable
+ public static Integer dbStatusToString(@Nullable CategorySortingMethod categorySortingMethod) {
+ return categorySortingMethod == null ? null : categorySortingMethod.getId();
+ }
+
+ @TypeConverter
+ public static Calendar calendarFromLong(Long value) {
+ Calendar calendar = Calendar.getInstance();
+ if (value == null) {
+ calendar.setTimeInMillis(0);
+ } else {
+ calendar.setTimeInMillis(value);
+ }
+ return calendar;
+ }
+
+ @TypeConverter
+ public static Long calendarToLong(Calendar calendar) {
+ return calendar == null ? 0 : calendar.getTimeInMillis();
+ }
+
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
new file mode 100644
index 00000000..7224d4eb
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
@@ -0,0 +1,251 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import java.io.Serializable;
+import java.util.Calendar;
+
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.Item;
+
+@Entity(
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId",
+ onDelete = ForeignKey.CASCADE
+ )
+ },
+ indices = {
+ @Index(name = "IDX_NOTE_ACCOUNTID", value = "accountId"),
+ @Index(name = "IDX_NOTE_CATEGORY", value = "category"),
+ @Index(name = "IDX_NOTE_FAVORITE", value = "favorite"),
+ @Index(name = "IDX_NOTE_MODIFIED", value = "modified"),
+ @Index(name = "IDX_NOTE_REMOTEID", value = "remoteId"),
+ @Index(name = "IDX_NOTE_STATUS", value = "status")
+ }
+)
+public class Note implements Serializable, Item {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @Nullable
+ private Long remoteId;
+ private long accountId;
+ @NonNull
+ private DBStatus status = DBStatus.VOID;
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String title = "";
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String category = "";
+ @Nullable
+ private Calendar modified;
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String content = "";
+ @ColumnInfo(defaultValue = "0")
+ private boolean favorite = false;
+ @Nullable
+ private String eTag;
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String excerpt = "";
+ @ColumnInfo(defaultValue = "0")
+ private int scrollY = 0;
+
+ public Note() {
+ super();
+ }
+
+ @Ignore
+ public Note(@Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String eTag) {
+ this.remoteId = remoteId;
+ this.title = title;
+ this.modified = modified;
+ this.content = content;
+ this.favorite = favorite;
+ this.category = category;
+ this.eTag = eTag;
+ }
+
+ @Ignore
+ public Note(long id, @Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String etag, @NonNull DBStatus status, long accountId, @NonNull String excerpt, int scrollY) {
+ this(remoteId, modified, title, content, category, favorite, etag);
+ this.id = id;
+ this.status = status;
+ this.accountId = accountId;
+ this.excerpt = excerpt;
+ this.scrollY = scrollY;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ @NonNull
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(@NonNull String category) {
+ this.category = category;
+ }
+
+ @Nullable
+ public Long getRemoteId() {
+ return remoteId;
+ }
+
+ public void setRemoteId(@Nullable Long remoteId) {
+ this.remoteId = remoteId;
+ }
+
+ public long getAccountId() {
+ return accountId;
+ }
+
+ public void setAccountId(long accountId) {
+ this.accountId = accountId;
+ }
+
+ @NonNull
+ public DBStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(@NonNull DBStatus status) {
+ this.status = status;
+ }
+
+ @NonNull
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(@NonNull String title) {
+ this.title = title;
+ }
+
+ @Nullable
+ public Calendar getModified() {
+ return modified;
+ }
+
+ public void setModified(@Nullable Calendar modified) {
+ this.modified = modified;
+ }
+
+ @NonNull
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(@NonNull String content) {
+ this.content = content;
+ }
+
+ public boolean getFavorite() {
+ return favorite;
+ }
+
+ public void setFavorite(boolean favorite) {
+ this.favorite = favorite;
+ }
+
+ @Nullable
+ public String getETag() {
+ return eTag;
+ }
+
+ public void setETag(@Nullable String eTag) {
+ this.eTag = eTag;
+ }
+
+ @NonNull
+ public String getExcerpt() {
+ return excerpt;
+ }
+
+ public void setExcerpt(@NonNull String excerpt) {
+ this.excerpt = excerpt;
+ }
+
+ public int getScrollY() {
+ return scrollY;
+ }
+
+ public void setScrollY(int scrollY) {
+ this.scrollY = scrollY;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Note)) return false;
+
+ Note note = (Note) o;
+
+ if (id != note.id) return false;
+ if (accountId != note.accountId) return false;
+ if (favorite != note.favorite) return false;
+ if (scrollY != note.scrollY) return false;
+ if (remoteId != null ? !remoteId.equals(note.remoteId) : note.remoteId != null)
+ return false;
+ if (status != note.status) return false;
+ if (!title.equals(note.title)) return false;
+ if (!category.equals(note.category)) return false;
+ if (modified != null ? !modified.equals(note.modified) : note.modified != null)
+ return false;
+ if (!content.equals(note.content)) return false;
+ if (eTag != null ? !eTag.equals(note.eTag) : note.eTag != null) return false;
+ return excerpt.equals(note.excerpt);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (id ^ (id >>> 32));
+ result = 31 * result + (remoteId != null ? remoteId.hashCode() : 0);
+ result = 31 * result + (int) (accountId ^ (accountId >>> 32));
+ result = 31 * result + status.hashCode();
+ result = 31 * result + title.hashCode();
+ result = 31 * result + category.hashCode();
+ result = 31 * result + (modified != null ? modified.hashCode() : 0);
+ result = 31 * result + content.hashCode();
+ result = 31 * result + (favorite ? 1 : 0);
+ result = 31 * result + (eTag != null ? eTag.hashCode() : 0);
+ result = 31 * result + excerpt.hashCode();
+ result = 31 * result + scrollY;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Note{" +
+ "id=" + id +
+ ", remoteId=" + remoteId +
+ ", accountId=" + accountId +
+ ", status=" + status +
+ ", title='" + title + '\'' +
+ ", category='" + category + '\'' +
+ ", modified=" + modified +
+ ", content='" + content + '\'' +
+ ", favorite=" + favorite +
+ ", eTag='" + eTag + '\'' +
+ ", excerpt='" + excerpt + '\'' +
+ ", scrollY=" + scrollY +
+ '}';
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/NotesListWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/NotesListWidgetData.java
new file mode 100644
index 00000000..da784d1d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/NotesListWidgetData.java
@@ -0,0 +1,89 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+
+import it.niedermann.owncloud.notes.widget.AbstractWidgetData;
+
+@Entity(
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId",
+ onDelete = ForeignKey.CASCADE
+ )
+ },
+ indices = {
+ @Index(name = "IDX_NOTESLISTWIDGETDATA_ACCOUNTID", value = "accountId"),
+ @Index(name = "IDX_NOTESLISTWIDGETDATA_CATEGORY", value = "category"),
+ @Index(name = "IDX_NOTESLISTWIDGETDATA_ACCOUNT_CATEGORY", value = {"accountId", "category"})
+ }
+)
+public class NotesListWidgetData extends AbstractWidgetData {
+
+ @Ignore
+ public static final int MODE_DISPLAY_ALL = 0;
+ @Ignore
+ public static final int MODE_DISPLAY_STARRED = 1;
+ @Ignore
+ public static final int MODE_DISPLAY_CATEGORY = 2;
+
+ @IntRange(from = 0, to = 2)
+ private int mode;
+
+ @Nullable
+ private String category;
+
+ @Nullable
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(@Nullable String category) {
+ this.category = category;
+ }
+
+ public void setMode(@IntRange(from = 0, to = 2) int mode) {
+ this.mode = mode;
+ }
+
+ @IntRange(from = 0, to = 2)
+ public int getMode() {
+ return mode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof NotesListWidgetData)) return false;
+ if (!super.equals(o)) return false;
+
+ NotesListWidgetData that = (NotesListWidgetData) o;
+
+ if (mode != that.mode) return false;
+ return category != null ? category.equals(that.category) : that.category == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + mode;
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "NotesListWidgetData{" +
+ "mode=" + mode +
+ ", category='" + category + '\'' +
+ '}';
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/SingleNoteWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/SingleNoteWidgetData.java
new file mode 100644
index 00000000..3e726242
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/SingleNoteWidgetData.java
@@ -0,0 +1,65 @@
+package it.niedermann.owncloud.notes.persistence.entity;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+
+import it.niedermann.owncloud.notes.widget.AbstractWidgetData;
+
+@Entity(
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId",
+ onDelete = ForeignKey.CASCADE
+ ),
+ @ForeignKey(
+ entity = Note.class,
+ parentColumns = "id",
+ childColumns = "noteId",
+ onDelete = ForeignKey.CASCADE
+ )
+ },
+ indices = {
+ @Index(name = "IDX_SINGLENOTEWIDGETDATA_ACCOUNTID", value = "accountId"),
+ @Index(name = "IDX_SINGLENOTEWIDGETDATA_NOTEID", value = "noteId"),
+ }
+)
+public class SingleNoteWidgetData extends AbstractWidgetData {
+ private long noteId;
+
+ public SingleNoteWidgetData() {
+ // Default constructor
+ }
+
+ @Ignore
+ public SingleNoteWidgetData(int id, long accountId, long noteId, int modeId) {
+ super(id, accountId, modeId);
+ setNoteId(noteId);
+ }
+
+ public long getNoteId() {
+ return noteId;
+ }
+
+ public void setNoteId(long noteId) {
+ this.noteId = noteId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SingleNoteWidgetData)) return false;
+
+ SingleNoteWidgetData that = (SingleNoteWidgetData) o;
+
+ return noteId == that.noteId;
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (noteId ^ (noteId >>> 32));
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java
index 4d45a3ce..5a739b7d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java
@@ -5,16 +5,27 @@ import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Map;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
-public class Migration_10_11 {
+public class Migration_10_11 extends Migration {
+ @NonNull
+ private final Context context;
+
+ public Migration_10_11(@NonNull Context context) {
+ super(10, 11);
+ this.context = context;
+ }
+
/**
* Changes the boolean for light / dark mode to {@link DarkModeSetting} to also be able to represent system default value
*/
- public Migration_10_11(@NonNull Context context) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
Map<String, ?> prefs = sharedPreferences.getAll();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java
index 6fd531c9..d37916e8 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java
@@ -4,15 +4,26 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
-import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+public class Migration_11_12 extends Migration {
+ @NonNull
+ private final Context context;
+
+ public Migration_11_12(@NonNull Context context) {
+ super(11, 12);
+ this.context = context;
+ }
-public class Migration_11_12 {
/**
* Adds columns to store the {@link ApiVersion} and the theme colors
*/
- public Migration_11_12(@NonNull SQLiteDatabase db, @NonNull Context context) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE ACCOUNTS ADD COLUMN API_VERSION TEXT");
db.execSQL("ALTER TABLE ACCOUNTS ADD COLUMN COLOR VARCHAR(6) NOT NULL DEFAULT '000000'");
db.execSQL("ALTER TABLE ACCOUNTS ADD COLUMN TEXT_COLOR VARCHAR(6) NOT NULL DEFAULT '0082C9'");
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java
index 77954c3f..07c405b6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java
@@ -4,15 +4,26 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.work.WorkManager;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
-public class Migration_12_13 {
+public class Migration_12_13 extends Migration {
+ @NonNull
+ private final Context context;
+
+ public Migration_12_13(@NonNull Context context) {
+ super(12, 13);
+ this.context = context;
+ }
+
/**
* Adds a column to store the ETag of the server {@link Capabilities}
*/
- public Migration_12_13(@NonNull SQLiteDatabase db, @NonNull Context context) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE ACCOUNTS ADD COLUMN CAPABILITIES_ETAG TEXT");
WorkManager.getInstance(context.getApplicationContext()).cancelUniqueWork("it.niedermann.owncloud.notes.persistence.SyncWorker");
WorkManager.getInstance(context.getApplicationContext()).cancelUniqueWork("SyncWorker");
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java
index 66a2f643..85e02617 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java
@@ -3,25 +3,38 @@ package it.niedermann.owncloud.notes.persistence.migration;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
+import androidx.room.OnConflictStrategy;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Map;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
-public class Migration_13_14 {
+public class Migration_13_14 extends Migration {
private static final String TAG = Migration_13_14.class.getSimpleName();
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final Runnable notifyWidgets;
+
+ public Migration_13_14(@NonNull Context context, @NonNull Runnable notifyWidgets) {
+ super(13, 14);
+ this.context = context;
+ this.notifyWidgets = notifyWidgets;
+ }
/**
* Move single note widget preferences to database
* https://github.com/stefan-niedermann/nextcloud-notes/issues/754
*/
- public Migration_13_14(@NonNull SQLiteDatabase db, @NonNull Context context, @NonNull Runnable notifyWidgets) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("CREATE TABLE WIDGET_SINGLE_NOTES ( " +
"ID INTEGER PRIMARY KEY, " +
"ACCOUNT_ID INTEGER, " +
@@ -60,7 +73,7 @@ public class Migration_13_14 {
migratedWidgetValues.put("ACCOUNT_ID", accountId);
migratedWidgetValues.put("NOTE_ID", noteId);
migratedWidgetValues.put("THEME_MODE", themeMode);
- db.insert("WIDGET_SINGLE_NOTES", null, migratedWidgetValues);
+ db.insert("WIDGET_SINGLE_NOTES", OnConflictStrategy.REPLACE, migratedWidgetValues);
} catch (Throwable t) {
Log.e(TAG, "Could not migrate widget {widgetId: " + widgetId + ", accountId: " + accountId + ", noteId: " + noteId + ", themeMode: " + themeMode + "}");
t.printStackTrace();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java
index e69c8ff1..6938e41c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java
@@ -5,16 +5,27 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.room.OnConflictStrategy;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
import java.util.Hashtable;
import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-public class Migration_14_15 {
+public class Migration_14_15 extends Migration {
+
+ public Migration_14_15() {
+ super(14, 15);
+ }
+
/**
* Normalize database (move category from string field to own table)
* https://github.com/stefan-niedermann/nextcloud-notes/issues/814
*/
- public Migration_14_15(SQLiteDatabase db) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
// Rename a tmp_NOTES table.
String tmpTableNotes = String.format("tmp_%s", "NOTES");
db.execSQL("ALTER TABLE NOTES RENAME TO " + tmpTableNotes);
@@ -44,7 +55,7 @@ public class Migration_14_15 {
// This is used to prevent too many searches in database
Hashtable<String, Integer> categoryTitleIdMap = new Hashtable<>();
int id = 1;
- Cursor tmpNotesCursor = db.rawQuery("SELECT * FROM " + tmpTableNotes, null);
+ Cursor tmpNotesCursor = db.query("SELECT * FROM " + tmpTableNotes, null);
while (tmpNotesCursor.moveToNext()) {
String categoryTitle = tmpNotesCursor.getString(8);
int accountId = tmpNotesCursor.getInt(2);
@@ -59,7 +70,7 @@ public class Migration_14_15 {
values.put("CATEGORY_ID", categoryId);
values.put("CATEGORY_ACCOUNT_ID", accountId);
values.put("CATEGORY_TITLE", categoryTitle);
- db.insert("CATEGORIES", null, values);
+ db.insert("CATEGORIES", OnConflictStrategy.REPLACE, values);
categoryTitleIdMap.put(categoryTitle, categoryId);
}
// Move the data in tmp_NOTES to NOTES
@@ -75,7 +86,7 @@ public class Migration_14_15 {
values.put("CATEGORY", categoryId);
values.put("ETAG", tmpNotesCursor.getString(9));
values.put("EXCERPT", tmpNotesCursor.getString(10));
- db.insert("NOTES", null, values);
+ db.insert("NOTES", OnConflictStrategy.REPLACE, values);
}
tmpNotesCursor.close();
db.execSQL("DROP TABLE IF EXISTS " + tmpTableNotes);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java
index 49056239..2732151f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java
@@ -4,25 +4,38 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
+import androidx.room.OnConflictStrategy;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Map;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
-public class Migration_15_16 {
+public class Migration_15_16 extends Migration {
private static final String TAG = Migration_15_16.class.getSimpleName();
+ @NonNull
+ private final Context context;
+ @NonNull
+ private final Runnable notifyWidgets;
+
+ public Migration_15_16(@NonNull Context context, @NonNull Runnable notifyWidgets) {
+ super(15, 16);
+ this.context = context;
+ this.notifyWidgets = notifyWidgets;
+ }
/**
* Moves note list widget preferences from {@link SharedPreferences} to database
* https://github.com/stefan-niedermann/nextcloud-notes/issues/832
*/
- public Migration_15_16(SQLiteDatabase db, @NonNull Context context, @NonNull Runnable notifyWidgets) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("CREATE TABLE WIDGET_NOTE_LISTS ( " +
"ID INTEGER PRIMARY KEY, " +
"ACCOUNT_ID INTEGER, " +
@@ -62,14 +75,7 @@ public class Migration_15_16 {
if (mode == 2) {
final String categoryTitle = sharedPreferences.getString(SP_CATEGORY_KEY + widgetId, null);
- Cursor cursor = db.query(
- "CATEGORIES",
- new String[]{"CATEGORY_ID"},
- "CATEGORY_TITLE = ? AND CATEGORY_ACCOUNT_ID = ? ",
- new String[]{categoryTitle, String.valueOf(accountId)},
- null,
- null,
- null);
+ Cursor cursor = db.query("SELECT CATEGORY_ID FROM CATEGORIES WHERE CATEGORY_TITLE = ? AND CATEGORY_ACCOUNT_ID = ?", new String[]{categoryTitle, String.valueOf(accountId)});
if (cursor.moveToNext()) {
categoryId = cursor.getInt(0);
} else {
@@ -84,7 +90,7 @@ public class Migration_15_16 {
migratedWidgetValues.put("CATEGORY_ID", categoryId);
migratedWidgetValues.put("MODE", mode);
migratedWidgetValues.put("THEME_MODE", themeMode);
- db.insert("WIDGET_NOTE_LISTS", null, migratedWidgetValues);
+ db.insert("WIDGET_NOTE_LISTS", OnConflictStrategy.REPLACE, migratedWidgetValues);
} catch (Throwable t) {
Log.e(TAG, "Could not migrate widget {widgetId: " + widgetId + ", accountId: " + accountId + ", mode: " + mode + ", categoryId: " + categoryId + ", themeMode: " + themeMode + "}");
t.printStackTrace();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_16_17.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_16_17.java
index 547cb6c7..b61262eb 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_16_17.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_16_17.java
@@ -1,15 +1,21 @@
package it.niedermann.owncloud.notes.persistence.migration;
-import android.database.sqlite.SQLiteDatabase;
-
import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+public class Migration_16_17 extends Migration {
+
+ public Migration_16_17() {
+ super(16, 17);
+ }
-public class Migration_16_17 {
/**
* Adds a column to store the current scroll position per note
* https://github.com/stefan-niedermann/nextcloud-notes/issues/227
*/
- public Migration_16_17(@NonNull SQLiteDatabase db) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE NOTES ADD COLUMN SCROLL_Y INTEGER DEFAULT 0");
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_17_18.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_17_18.java
index 6faa9016..1a7a96e7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_17_18.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_17_18.java
@@ -3,12 +3,20 @@ package it.niedermann.owncloud.notes.persistence.migration;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+public class Migration_17_18 extends Migration {
+
+ public Migration_17_18() {
+ super(17, 18);
+ }
-public class Migration_17_18 {
/**
* Add a new column to store the sorting method for a category note list
*/
- public Migration_17_18(@NonNull SQLiteDatabase db) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE CATEGORIES ADD COLUMN CATEGORY_SORTING_METHOD INTEGER DEFAULT 0");
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
index a3205db0..343f9e81 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
@@ -4,19 +4,29 @@ import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import com.bumptech.glide.Glide;
-public class Migration_18_19 {
+public class Migration_18_19 extends Migration {
+
private static final String TAG = Migration_18_19.class.getSimpleName();
+ @NonNull
+ private final Context context;
+
+
+ public Migration_18_19(@NonNull Context context) {
+ super(18, 19);
+ this.context = context;
+ }
/**
* Clears the {@link Glide} disk cache to fix wrong avatars in a multi user setup
* https://github.com/stefan-niedermann/nextcloud-deck/issues/531
- *
- * @param context {@link Context}
*/
- public Migration_18_19(@NonNull Context context) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
new Thread(() -> {
Log.i(TAG, "Clearing Glide disk cache");
Glide.get(context.getApplicationContext()).clearDiskCache();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_19_20.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_19_20.java
new file mode 100644
index 00000000..7a1d6ff3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_19_20.java
@@ -0,0 +1,30 @@
+package it.niedermann.owncloud.notes.persistence.migration;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceManager;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+public class Migration_19_20 extends Migration {
+
+ @NonNull
+ private final Context context;
+
+ /**
+ * Removes <code>branding</code> from {@link SharedPreferences} because we do no longer allow to disable it.
+ *
+ * @param context {@link Context}
+ */
+ public Migration_19_20(@NonNull Context context) {
+ super(19, 20);
+ this.context = context;
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit().remove("branding").apply();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_20_21.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_20_21.java
new file mode 100644
index 00000000..d9ed2041
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_20_21.java
@@ -0,0 +1,232 @@
+package it.niedermann.owncloud.notes.persistence.migration;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.room.OnConflictStrategy;
+import androidx.room.RoomDatabase;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import it.niedermann.android.util.ColorUtil;
+
+public final class Migration_20_21 extends Migration {
+
+ public Migration_20_21() {
+ super(20, 21);
+ }
+
+ /**
+ * From {@link SQLiteOpenHelper} to {@link RoomDatabase}
+ * https://github.com/stefan-niedermann/nextcloud-deck/issues/531
+ */
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
+ dropOldIndices(db);
+
+ createNewTables(db);
+ createNewIndices(db);
+
+ migrateAccounts(db);
+ migrateCategories(db);
+ migrateNotes(db);
+ migrateNotesListWidgets(db);
+ migrateSingleNotesWidgets(db);
+
+ dropOldTables(db);
+ }
+
+ private static void dropOldIndices(@NonNull SupportSQLiteDatabase db) {
+ db.execSQL("DROP INDEX IF EXISTS ACCOUNTS_URL_idx");
+ db.execSQL("DROP INDEX IF EXISTS ACCOUNTS_USERNAME_idx");
+ db.execSQL("DROP INDEX IF EXISTS ACCOUNTS_ACCOUNT_NAME_idx");
+ db.execSQL("DROP INDEX IF EXISTS ACCOUNTS_ETAG_idx");
+ db.execSQL("DROP INDEX IF EXISTS ACCOUNTS_MODIFIED_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_REMOTEID_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_ACCOUNT_ID_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_STATUS_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_FAVORITE_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_CATEGORY_idx");
+ db.execSQL("DROP INDEX IF EXISTS NOTES_MODIFIED_idx");
+ }
+
+ private static void createNewTables(@NonNull SupportSQLiteDatabase db) {
+ db.execSQL("CREATE TABLE `Account` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL DEFAULT '', `userName` TEXT NOT NULL DEFAULT '', `accountName` TEXT NOT NULL DEFAULT '', `eTag` TEXT, `modified` INTEGER, `apiVersion` TEXT, `color` INTEGER NOT NULL DEFAULT -16743735, `textColor` INTEGER NOT NULL DEFAULT -16777216, `capabilitiesETag` TEXT)");
+ db.execSQL("CREATE TABLE `CategoryOptions` (`accountId` INTEGER NOT NULL, `category` TEXT NOT NULL, `sortingMethod` INTEGER, PRIMARY KEY(`accountId`, `category`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ db.execSQL("CREATE TABLE `Note` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remoteId` INTEGER, `accountId` INTEGER NOT NULL, `status` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT '', `category` TEXT NOT NULL DEFAULT '', `modified` INTEGER, `content` TEXT NOT NULL DEFAULT '', `favorite` INTEGER NOT NULL DEFAULT 0, `eTag` TEXT, `excerpt` TEXT NOT NULL DEFAULT '', `scrollY` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ db.execSQL("CREATE TABLE `NotesListWidgetData` (`mode` INTEGER NOT NULL, `category` TEXT, `id` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `themeMode` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ db.execSQL("CREATE TABLE `SingleNoteWidgetData` (`noteId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `themeMode` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`noteId`) REFERENCES `Note`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ }
+
+ private static void createNewIndices(@NonNull SupportSQLiteDatabase db) {
+ db.execSQL("CREATE INDEX `IDX_ACCOUNT_ACCOUNTNAME` ON `Account` (`accountName`)");
+ db.execSQL("CREATE INDEX `IDX_ACCOUNT_ETAG` ON `Account` (`eTag`)");
+ db.execSQL("CREATE INDEX `IDX_ACCOUNT_MODIFIED` ON `Account` (`modified`)");
+ db.execSQL("CREATE INDEX `IDX_ACCOUNT_URL` ON `Account` (`url`)");
+ db.execSQL("CREATE INDEX `IDX_ACCOUNT_USERNAME` ON `Account` (`userName`)");
+ db.execSQL("CREATE INDEX `IDX_CATEGORIYOPTIONS_ACCOUNTID` ON `CategoryOptions` (`accountId`)");
+ db.execSQL("CREATE INDEX `IDX_CATEGORIYOPTIONS_CATEGORY` ON `CategoryOptions` (`category`)");
+ db.execSQL("CREATE INDEX `IDX_CATEGORIYOPTIONS_SORTING_METHOD` ON `CategoryOptions` (`sortingMethod`)");
+ db.execSQL("CREATE INDEX `IDX_NOTESLISTWIDGETDATA_ACCOUNTID` ON `NotesListWidgetData` (`accountId`)");
+ db.execSQL("CREATE INDEX `IDX_NOTESLISTWIDGETDATA_CATEGORY` ON `NotesListWidgetData` (`category`)");
+ db.execSQL("CREATE INDEX `IDX_NOTESLISTWIDGETDATA_ACCOUNT_CATEGORY` ON `NotesListWidgetData` (`accountId`, `category`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_ACCOUNTID` ON `Note` (`accountId`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_CATEGORY` ON `Note` (`category`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_FAVORITE` ON `Note` (`favorite`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_MODIFIED` ON `Note` (`modified`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_REMOTEID` ON `Note` (`remoteId`)");
+ db.execSQL("CREATE INDEX `IDX_NOTE_STATUS` ON `Note` (`status`)");
+ db.execSQL("CREATE INDEX `IDX_SINGLENOTEWIDGETDATA_ACCOUNTID` ON `SingleNoteWidgetData` (`accountId`)");
+ db.execSQL("CREATE INDEX `IDX_SINGLENOTEWIDGETDATA_NOTEID` ON `SingleNoteWidgetData` (`noteId`)");
+
+ db.execSQL("CREATE UNIQUE INDEX `IDX_UNIQUE_CATEGORYOPTIONS_ACCOUNT_CATEGORY` ON `CategoryOptions` (`accountId`, `category`)");
+
+ db.execSQL("CREATE TRIGGER TRG_CLEANUP_CATEGORIES_DEL AFTER DELETE ON Note BEGIN DELETE FROM CategoryOptions WHERE CategoryOptions.category NOT IN (SELECT Note.category FROM Note WHERE Note.accountId = CategoryOptions.accountId); END;");
+ db.execSQL("CREATE TRIGGER TRG_CLEANUP_CATEGORIES_UPD AFTER UPDATE ON Note BEGIN DELETE FROM CategoryOptions WHERE CategoryOptions.category NOT IN (SELECT Note.category FROM Note WHERE Note.accountId = CategoryOptions.accountId); END;");
+
+ }
+
+ private static void migrateAccounts(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT * FROM ACCOUNTS", null);
+ final ContentValues values = new ContentValues(10);
+
+ final int COLUMN_POSITION_ID = cursor.getColumnIndex("ID");
+ final int COLUMN_POSITION_URL = cursor.getColumnIndex("URL");
+ final int COLUMN_POSITION_USERNAME = cursor.getColumnIndex("USERNAME");
+ final int COLUMN_POSITION_ACCOUNT_NAME = cursor.getColumnIndex("ACCOUNT_NAME");
+ final int COLUMN_POSITION_ETAG = cursor.getColumnIndex("ETAG");
+ final int COLUMN_POSITION_MODIFIED = cursor.getColumnIndex("MODIFIED");
+ final int COLUMN_POSITION_API_VERSION = cursor.getColumnIndex("API_VERSION");
+ final int COLUMN_POSITION_COLOR = cursor.getColumnIndex("COLOR");
+ final int COLUMN_POSITION_TEXT_COLOR = cursor.getColumnIndex("TEXT_COLOR");
+ final int COLUMN_POSITION_CAPABILITIES_ETAG = cursor.getColumnIndex("CAPABILITIES_ETAG");
+
+ while (cursor.moveToNext()) {
+ values.put("ID", cursor.getInt(COLUMN_POSITION_ID));
+ values.put("URL", cursor.getString(COLUMN_POSITION_URL));
+ values.put("USERNAME", cursor.getString(COLUMN_POSITION_USERNAME));
+ values.put("ACCOUNTNAME", cursor.getString(COLUMN_POSITION_ACCOUNT_NAME));
+ values.put("ETAG", cursor.getString(COLUMN_POSITION_ETAG));
+ values.put("MODIFIED", cursor.getLong(COLUMN_POSITION_MODIFIED) * 1_000);
+ values.put("APIVERSION", cursor.getString(COLUMN_POSITION_API_VERSION));
+ try {
+ values.put("COLOR", Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(cursor.getString(COLUMN_POSITION_COLOR))));
+ } catch (Exception e) {
+ e.printStackTrace();
+ values.put("COLOR", -16743735);
+ }
+ try {
+ values.put("TEXTCOLOR", Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(cursor.getString(COLUMN_POSITION_TEXT_COLOR))));
+ } catch (Exception e) {
+ e.printStackTrace();
+ values.put("TEXTCOLOR", -16777216);
+ }
+ values.put("CAPABILITIESETAG", cursor.getString(COLUMN_POSITION_CAPABILITIES_ETAG));
+ db.insert("ACCOUNT", OnConflictStrategy.REPLACE, values);
+ }
+ cursor.close();
+ }
+
+ private static void migrateCategories(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT * FROM CATEGORIES", null);
+ final ContentValues values = new ContentValues(3);
+
+ final int COLUMN_POSITION_ACCOUNT_ID = cursor.getColumnIndex("CATEGORY_ACCOUNT_ID");
+ final int COLUMN_POSITION_TITLE = cursor.getColumnIndex("CATEGORY_TITLE");
+ final int COLUMN_POSITION_SORTING_METHOD = cursor.getColumnIndex("CATEGORY_SORTING_METHOD");
+
+ while (cursor.moveToNext()) {
+ values.put("ACCOUNTID", cursor.getInt(COLUMN_POSITION_ACCOUNT_ID));
+ values.put("CATEGORY", cursor.getString(COLUMN_POSITION_TITLE));
+ values.put("SORTINGMETHOD", cursor.getInt(COLUMN_POSITION_SORTING_METHOD));
+ db.insert("CATEGORYOPTIONS", OnConflictStrategy.REPLACE, values);
+ }
+ cursor.close();
+ }
+
+ private static void migrateNotes(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT NOTES.*, CATEGORIES.category_title as `CAT_TITLE` FROM NOTES LEFT JOIN CATEGORIES ON NOTES.category = CATEGORIES.category_id", null);
+ final ContentValues values = new ContentValues(12);
+
+ final int COLUMN_POSITION_ID = cursor.getColumnIndex("ID");
+ final int COLUMN_POSITION_REMOTEID = cursor.getColumnIndex("REMOTEID");
+ final int COLUMN_POSITION_ACCOUNT_ID = cursor.getColumnIndex("ACCOUNT_ID");
+ final int COLUMN_POSITION_STATUS = cursor.getColumnIndex("STATUS");
+ final int COLUMN_POSITION_TITLE = cursor.getColumnIndex("TITLE");
+ final int COLUMN_POSITION_MODIFIED = cursor.getColumnIndex("MODIFIED");
+ final int COLUMN_POSITION_CONTENT = cursor.getColumnIndex("CONTENT");
+ final int COLUMN_POSITION_FAVORITE = cursor.getColumnIndex("FAVORITE");
+ final int COLUMN_POSITION_CAT_TITLE = cursor.getColumnIndex("CAT_TITLE");
+ final int COLUMN_POSITION_ETAG = cursor.getColumnIndex("ETAG");
+ final int COLUMN_POSITION_EXCERPT = cursor.getColumnIndex("EXCERPT");
+ final int COLUMN_POSITION_SCROLL_Y = cursor.getColumnIndex("SCROLL_Y");
+
+ while (cursor.moveToNext()) {
+ values.put("ID", cursor.getInt(COLUMN_POSITION_ID));
+ values.put("REMOTEID", cursor.getInt(COLUMN_POSITION_REMOTEID));
+ values.put("ACCOUNTID", cursor.getInt(COLUMN_POSITION_ACCOUNT_ID));
+ values.put("STATUS", cursor.getString(COLUMN_POSITION_STATUS));
+ values.put("TITLE", cursor.getString(COLUMN_POSITION_TITLE));
+ values.put("MODIFIED", cursor.getLong(COLUMN_POSITION_MODIFIED) * 1_000);
+ values.put("CONTENT", cursor.getString(COLUMN_POSITION_CONTENT));
+ values.put("FAVORITE", cursor.getInt(COLUMN_POSITION_FAVORITE));
+ values.put("CATEGORY", cursor.getString(COLUMN_POSITION_CAT_TITLE));
+ values.put("ETAG", cursor.getString(COLUMN_POSITION_ETAG));
+ values.put("EXCERPT", cursor.getString(COLUMN_POSITION_EXCERPT));
+ values.put("SCROLLY", cursor.getString(COLUMN_POSITION_SCROLL_Y));
+ db.insert("NOTE", OnConflictStrategy.REPLACE, values);
+ }
+ cursor.close();
+ }
+
+ private static void migrateNotesListWidgets(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT WIDGET_NOTE_LISTS.*, CATEGORIES.category_title as `CATEGORY` FROM WIDGET_NOTE_LISTS LEFT JOIN CATEGORIES ON WIDGET_NOTE_LISTS.CATEGORY_ID = CATEGORIES.category_id", null);
+ final ContentValues values = new ContentValues(5);
+
+ final int COLUMN_POSITION_ID = cursor.getColumnIndex("ID");
+ final int COLUMN_POSITION_ACCOUNT_ID = cursor.getColumnIndex("ACCOUNT_ID");
+ final int COLUMN_POSITION_CATEGORY = cursor.getColumnIndex("CATEGORY");
+ final int COLUMN_POSITION_MODE = cursor.getColumnIndex("MODE");
+ final int COLUMN_POSITION_THEME_MODE = cursor.getColumnIndex("THEME_MODE");
+
+ while (cursor.moveToNext()) {
+ values.put("ID", cursor.getInt(COLUMN_POSITION_ID));
+ values.put("ACCOUNTID", cursor.getInt(COLUMN_POSITION_ACCOUNT_ID));
+ values.put("CATEGORY", cursor.getString(COLUMN_POSITION_CATEGORY));
+ values.put("MODE", cursor.getInt(COLUMN_POSITION_MODE));
+ values.put("THEMEMODE", cursor.getInt(COLUMN_POSITION_THEME_MODE));
+ db.insert("NOTESLISTWIDGETDATA", OnConflictStrategy.REPLACE, values);
+ }
+ cursor.close();
+ }
+
+ private static void migrateSingleNotesWidgets(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT * FROM WIDGET_SINGLE_NOTES", null);
+ final ContentValues values = new ContentValues(4);
+
+ final int COLUMN_POSITION_ID = cursor.getColumnIndex("ID");
+ final int COLUMN_POSITION_ACCOUNT_ID = cursor.getColumnIndex("ACCOUNT_ID");
+ final int COLUMN_POSITION_NOTE_ID = cursor.getColumnIndex("NOTE_ID");
+ final int COLUMN_POSITION_THEME_MODE = cursor.getColumnIndex("THEME_MODE");
+
+ while (cursor.moveToNext()) {
+ values.put("ID", cursor.getInt(COLUMN_POSITION_ID));
+ values.put("ACCOUNTID", cursor.getInt(COLUMN_POSITION_ACCOUNT_ID));
+ values.put("NOTEID", cursor.getInt(COLUMN_POSITION_NOTE_ID));
+ values.put("THEMEMODE", cursor.getInt(COLUMN_POSITION_THEME_MODE));
+ db.insert("SINGLENOTEWIDGETDATA", OnConflictStrategy.REPLACE, values);
+ }
+ cursor.close();
+ }
+
+ private static void dropOldTables(@NonNull SupportSQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS WIDGET_SINGLE_NOTES");
+ db.execSQL("DROP TABLE IF EXISTS WIDGET_NOTE_LISTS");
+ db.execSQL("DROP TABLE IF EXISTS CATEGORIES");
+ db.execSQL("DROP TABLE IF EXISTS NOTES");
+ db.execSQL("DROP TABLE IF EXISTS ACCOUNTS");
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java
deleted file mode 100644
index aa807944..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package it.niedermann.owncloud.notes.persistence.migration;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
-
-public class Migration_4_5 {
- /**
- * Differentiate between local id and remote id
- */
- public Migration_4_5(@NonNull SQLiteDatabase db) {
- db.execSQL("ALTER TABLE NOTES ADD COLUMN REMOTEID INTEGER");
- db.execSQL("UPDATE NOTES SET REMOTEID=ID WHERE (REMOTEID IS NULL OR REMOTEID=0) AND STATUS!=?", new String[]{"LOCAL_CREATED"});
- db.execSQL("UPDATE NOTES SET REMOTEID=0, STATUS=? WHERE STATUS=?", new String[]{"LOCAL_EDITED", "LOCAL_CREATED"});
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_5_6.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_5_6.java
deleted file mode 100644
index 184a6033..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_5_6.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package it.niedermann.owncloud.notes.persistence.migration;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
-
-public class Migration_5_6 {
- /**
- * Adds a column to support marking notes as favorite
- */
- public Migration_5_6(@NonNull SQLiteDatabase db) {
- db.execSQL("ALTER TABLE NOTES ADD COLUMN FAVORITE INTEGER DEFAULT 0");
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java
deleted file mode 100644
index e7d0eadd..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package it.niedermann.owncloud.notes.persistence.migration;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
-
-import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-
-public class Migration_6_7 {
- /**
- * Adds columns for category support and ETags
- */
- public Migration_6_7(@NonNull SQLiteDatabase db) {
- DatabaseIndexUtil.dropIndexes(db);
- db.execSQL("ALTER TABLE NOTES ADD COLUMN CATEGORY TEXT NOT NULL DEFAULT ''");
- db.execSQL("ALTER TABLE NOTES ADD COLUMN ETAG TEXT");
- DatabaseIndexUtil.createIndex(db, "NOTES", "REMOTEID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED");
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java
deleted file mode 100644
index d1fd40c0..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package it.niedermann.owncloud.notes.persistence.migration;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
-
-import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-
-public class Migration_7_8 {
- public Migration_7_8(@NonNull SQLiteDatabase db) {
- final String table_temp = "NOTES_TEMP";
- db.execSQL("CREATE TABLE " + table_temp + " ( " +
- "ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "REMOTEID INTEGER, " +
- "STATUS VARCHAR(50), " +
- "TITLE TEXT, " +
- "MODIFIED INTEGER DEFAULT 0, " +
- "CONTENT TEXT, " +
- "FAVORITE INTEGER DEFAULT 0, " +
- "CATEGORY TEXT NOT NULL DEFAULT '', " +
- "ETAG TEXT)");
- DatabaseIndexUtil.createIndex(db, table_temp, "REMOTEID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED");
- db.execSQL(String.format("INSERT INTO %s(%s,%s,%s,%s,%s,%s,%s,%s,%s) ", table_temp, "ID", "REMOTEID", "STATUS", "TITLE", "MODIFIED", "CONTENT", "FAVORITE", "CATEGORY", "ETAG")
- + String.format("SELECT %s,%s,%s,%s,strftime('%%s',%s),%s,%s,%s,%s FROM %s", "ID", "REMOTEID", "STATUS", "TITLE", "MODIFIED", "CONTENT", "FAVORITE", "CATEGORY", "ETAG", "NOTES"));
- db.execSQL("DROP TABLE NOTES");
- db.execSQL(String.format("ALTER TABLE %s RENAME TO %s", table_temp, "NOTES"));
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java
deleted file mode 100644
index 03e171f3..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package it.niedermann.owncloud.notes.persistence.migration;
-
-import android.appwidget.AppWidgetManager;
-import android.content.ComponentName;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.core.util.Consumer;
-import androidx.preference.PreferenceManager;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import it.niedermann.owncloud.notes.widget.notelist.NoteListWidget;
-import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget;
-import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-
-public class Migration_8_9 {
-
- private static final String TAG = Migration_8_9.class.getSimpleName();
-
- /**
- * Adds an account table for multi account usage in combination with SingleSignOn
- */
- public Migration_8_9(@NonNull SQLiteDatabase db, @NonNull Context context, @NonNull Consumer<SQLiteDatabase> recreateDatabase, @NonNull Runnable notifyWidgets) {
- // Create accounts table
- db.execSQL("CREATE TABLE ACCOUNTS ( " +
- "ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "URL TEXT, " +
- "USERNAME TEXT, " +
- "ACCOUNT_NAME TEXT UNIQUE, " +
- "ETAG TEXT, " +
- "MODIFIED INTEGER)");
- DatabaseIndexUtil.createIndex(db, "ACCOUNTS", "URL", "USERNAME", "ACCOUNT_NAME", "ETAG", "MODIFIED");
-
- // Add accountId to notes table
- db.execSQL("ALTER TABLE NOTES ADD COLUMN ACCOUNT_ID INTEGER NOT NULL DEFAULT 0");
- DatabaseIndexUtil.createIndex(db, "NOTES", "ACCOUNT_ID");
-
- // Migrate existing account from SharedPreferences
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- String username = sharedPreferences.getString("settingsUsername", "");
- String url = sharedPreferences.getString("settingsUrl", "");
- if (!url.isEmpty() && url.endsWith("/")) {
- url = url.substring(0, url.length() - 1);
- try {
- String accountName = username + "@" + new URL(url).getHost();
-
- ContentValues migratedAccountValues = new ContentValues();
- migratedAccountValues.put("URL", url);
- migratedAccountValues.put("USERNAME", username);
- migratedAccountValues.put("ACCOUNT_NAME", accountName);
- db.insert("ACCOUNTS", null, migratedAccountValues);
-
- // After successful insertion of migrated account, set accountId to 1 in each note
- ContentValues values = new ContentValues();
- values.put("ACCOUNT_ID", 1);
- db.update("NOTES", values, "ACCOUNT_ID = ?", new String[]{"NULL"});
-
- // Add FOREIGN_KEY constraint
- final String table_temp = "NOTES_TEMP";
- db.execSQL(String.format("ALTER TABLE %s RENAME TO %s", "NOTES", table_temp));
-
- db.execSQL("CREATE TABLE NOTES ( " +
- "ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "REMOTEID INTEGER, " +
- "ACCOUNT_ID INTEGER, " +
- "STATUS VARCHAR(50), " +
- "TITLE TEXT, " +
- "MODIFIED INTEGER DEFAULT 0, " +
- "CONTENT TEXT, " +
- "FAVORITE INTEGER DEFAULT 0, " +
- "CATEGORY TEXT NOT NULL DEFAULT '', " +
- "ETAG TEXT," +
- "FOREIGN KEY(ACCOUNT_ID) REFERENCES ACCOUNTS(ID))");
- DatabaseIndexUtil.createIndex(db, "NOTES", "REMOTEID", "ACCOUNT_ID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED");
-
- db.execSQL(String.format("INSERT INTO %s(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ", "NOTES", "ID", "ACCOUNT_ID", "REMOTEID", "STATUS", "TITLE", "MODIFIED", "CONTENT", "FAVORITE", "CATEGORY", "ETAG")
- + String.format("SELECT %s,%s,%s,%s,%s,%s,%s,%s,%s,%s FROM %s", "ID", values.get("ACCOUNT_ID"), "REMOTEID", "STATUS", "TITLE", "MODIFIED", "CONTENT", "FAVORITE", "CATEGORY", "ETAG", table_temp));
- db.execSQL(String.format("DROP TABLE %s;", table_temp));
-
- AppWidgetManager awm = AppWidgetManager.getInstance(context);
- SharedPreferences.Editor editor = sharedPreferences.edit();
-
- // Add accountId '1' to any existing (and configured) appwidgets
- int[] appWidgetIdsNLW = awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class));
- int[] appWidgetIdsSNW = awm.getAppWidgetIds(new ComponentName(context, SingleNoteWidget.class));
-
- final String WIDGET_MODE_KEY = "NLW_mode";
- final String ACCOUNT_ID_KEY = "NLW_account";
-
- for (int appWidgetId : appWidgetIdsNLW) {
- if (sharedPreferences.getInt(WIDGET_MODE_KEY + appWidgetId, -1) >= 0) {
- editor.putLong(ACCOUNT_ID_KEY + appWidgetId, 1);
- }
- }
-
- for (int appWidgetId : appWidgetIdsSNW) {
- if (sharedPreferences.getLong("single_note_widget" + appWidgetId, -1) >= 0) {
- editor.putLong("SNW_accountId" + appWidgetId, 1);
- }
- }
-
- notifyWidgets.run();
-
- // Clean up no longer needed SharedPreferences
- editor.remove("notes_last_etag");
- editor.remove("notes_last_modified");
- editor.remove("settingsUrl");
- editor.remove("settingsUsername");
- editor.remove("settingsPassword");
- editor.apply();
- } catch (MalformedURLException e) {
- Log.e(TAG, "Previous URL could not be parsed. Recreating database...");
- e.printStackTrace();
- recreateDatabase.accept(db);
- }
- } else {
- Log.e(TAG, "Previous URL is empty or does not end with a '/' character. Recreating database...");
- recreateDatabase.accept(db);
- }
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java
index b17b8675..7cdab8c0 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java
@@ -5,21 +5,30 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
+import androidx.room.OnConflictStrategy;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
-public class Migration_9_10 {
+public class Migration_9_10 extends Migration {
+
+ public Migration_9_10() {
+ super(9, 10);
+ }
+
/**
* Adds a column to store excerpt instead of regenerating it each time
* https://github.com/stefan-niedermann/nextcloud-notes/issues/528
*/
- public Migration_9_10(@NonNull SQLiteDatabase db) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("ALTER TABLE NOTES ADD COLUMN EXCERPT INTEGER NOT NULL DEFAULT ''");
- Cursor cursor = db.query("NOTES", new String[]{"ID", "CONTENT", "TITLE"}, null, null, null, null, null, null);
+ Cursor cursor = db.query("NOTES", new String[]{"ID", "CONTENT", "TITLE"});
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put("EXCERPT", NoteUtil.generateNoteExcerpt(cursor.getString(1), cursor.getString(2)));
- db.update("NOTES", values, "ID" + " = ? ", new String[]{cursor.getString(0)});
+ db.update("NOTES", OnConflictStrategy.REPLACE, values, "ID" + " = ? ", new String[]{cursor.getString(0)});
}
cursor.close();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java
index e808a3a0..f872aac3 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java
@@ -4,8 +4,8 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
-import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.LockedActivity;
+import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ActivityPreferencesBinding;
/**
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java
index 4a890be0..8af467fb 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java
@@ -12,13 +12,13 @@ import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
+import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.Branded;
import it.niedermann.owncloud.notes.branding.BrandedSwitchPreference;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.persistence.SyncWorker;
import it.niedermann.owncloud.notes.shared.util.DeviceCredentialUtil;
-import it.niedermann.owncloud.notes.NotesApplication;
import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets;
@@ -29,8 +29,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
private BrandedSwitchPreference fontPref;
private BrandedSwitchPreference lockPref;
private BrandedSwitchPreference wifiOnlyPref;
- private BrandedSwitchPreference brandingPref;
private BrandedSwitchPreference gridViewPref;
+ private BrandedSwitchPreference preventScreenCapturePref;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -43,20 +43,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
fontPref = findPreference(getString(R.string.pref_key_font));
- brandingPref = findPreference(getString(R.string.pref_key_branding));
- if (brandingPref != null) {
- brandingPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
- updateNoteListWidgets(requireContext());
- final Boolean branding = (Boolean) newValue;
- Log.v(TAG, "branding: " + branding);
- requireActivity().setResult(Activity.RESULT_OK);
- ActivityCompat.recreate(requireActivity());
- return true;
- });
- } else {
- Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\"");
- }
-
gridViewPref = findPreference(getString(R.string.pref_key_gridview));
if (gridViewPref != null) {
gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
@@ -67,19 +53,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
return true;
});
} else {
- Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\"");
+ Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_gridview) + "\"");
+ }
+
+ preventScreenCapturePref = findPreference(getString(R.string.pref_key_prevent_screen_capture));
+ if (preventScreenCapturePref == null) {
+ Log.e(TAG, "Could not find \"" + getString(R.string.pref_key_prevent_screen_capture) + "\"-preference.");
}
lockPref = findPreference(getString(R.string.pref_key_lock));
if (lockPref != null) {
if (!DeviceCredentialUtil.areCredentialsAvailable(requireContext())) {
lockPref.setVisible(false);
- Preference securityCategory = findPreference(getString(R.string.pref_category_security));
- if (securityCategory != null) {
- securityCategory.setVisible(false);
- } else {
- Log.e(TAG, "Could not find preference " + getString(R.string.pref_category_security));
- }
} else {
lockPref.setOnPreferenceChangeListener((preference, newValue) -> {
NotesApplication.setLockedPreference((Boolean) newValue);
@@ -132,7 +117,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
fontPref.applyBrand(mainColor, textColor);
lockPref.applyBrand(mainColor, textColor);
wifiOnlyPref.applyBrand(mainColor, textColor);
- brandingPref.applyBrand(mainColor, textColor);
gridViewPref.applyBrand(mainColor, textColor);
+ preventScreenCapturePref.applyBrand(mainColor, textColor);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java
index dd28f431..6804ee62 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java
@@ -10,16 +10,16 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class AccountChooserAdapter extends RecyclerView.Adapter<AccountChooserViewHolder> {
@NonNull
- private final List<LocalAccount> localAccounts;
+ private final List<Account> localAccounts;
@NonNull
- private final Consumer<LocalAccount> targetAccountConsumer;
+ private final Consumer<Account> targetAccountConsumer;
- public AccountChooserAdapter(@NonNull List<LocalAccount> localAccounts, @NonNull Consumer<LocalAccount> targetAccountConsumer) {
+ public AccountChooserAdapter(@NonNull List<Account> localAccounts, @NonNull Consumer<Account> targetAccountConsumer) {
super();
this.localAccounts = localAccounts;
this.targetAccountConsumer = targetAccountConsumer;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
index 143adabc..7688b1b1 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
@@ -11,7 +11,7 @@ import com.bumptech.glide.request.RequestOptions;
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
public class AccountChooserViewHolder extends RecyclerView.ViewHolder {
private final ItemAccountChooseBinding binding;
@@ -21,7 +21,7 @@ public class AccountChooserViewHolder extends RecyclerView.ViewHolder {
this.binding = binding;
}
- public void bind(LocalAccount localAccount, Consumer<LocalAccount> targetAccountConsumer) {
+ public void bind(Account localAccount, Consumer<Account> targetAccountConsumer) {
Glide
.with(binding.accountItemAvatar.getContext())
.load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
index b182ca63..1c1bed3d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
@@ -1,7 +1,9 @@
package it.niedermann.owncloud.notes.shared.model;
+import android.graphics.Color;
import android.util.Log;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,10 +13,12 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import org.json.JSONException;
import org.json.JSONObject;
+import it.niedermann.android.util.ColorUtil;
+
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
/**
- * This entity class is used to return relevant data of the HTTP reponse.
+ * This entity class is used to return relevant data of the HTTP response.
*/
public class Capabilities {
@@ -32,10 +36,13 @@ public class Capabilities {
private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text";
private String apiVersion = null;
- private String color = null;
- private String textColor = null;
+
+ @ColorInt
+ private Integer color = -16743735;
+ @ColorInt
+ private Integer textColor = -16777216;
@Nullable
- private String eTag;
+ private final String eTag;
public Capabilities(@NonNull String response, @Nullable String eTag) throws NextcloudHttpRequestFailedException {
this.eTag = eTag;
@@ -64,10 +71,18 @@ public class Capabilities {
if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) {
final JSONObject theming = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_THEMING);
if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) {
- this.color = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR);
+ try {
+ this.color = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
}
if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) {
- this.textColor = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT);
+ try {
+ this.textColor = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
}
}
}
@@ -81,17 +96,17 @@ public class Capabilities {
return apiVersion;
}
- public String getColor() {
- return color;
+ @Nullable
+ public String getETag() {
+ return eTag;
}
- public String getTextColor() {
- return textColor;
+ public Integer getColor() {
+ return color;
}
- @Nullable
- public String getETag() {
- return eTag;
+ public Integer getTextColor() {
+ return textColor;
}
@NonNull
@@ -99,8 +114,9 @@ public class Capabilities {
public String toString() {
return "Capabilities{" +
"apiVersion='" + apiVersion + '\'' +
- ", color='" + color + '\'' +
- ", textColor='" + textColor + '\'' +
+ ", color=" + color +
+ ", textColor=" + textColor +
+ ", eTag='" + eTag + '\'' +
'}';
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java
deleted file mode 100644
index ab1d10a1..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import androidx.annotation.Nullable;
-
-import java.io.Serializable;
-
-public class Category implements Serializable {
-
- @Nullable
- public final String category;
- @Nullable
- public final Boolean favorite;
-
- public Category(@Nullable String category, @Nullable Boolean favorite) {
- this.category = category;
- this.favorite = favorite;
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java
index 35ab8b66..94bcda38 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java
@@ -1,41 +1,48 @@
package it.niedermann.owncloud.notes.shared.model;
public enum CategorySortingMethod {
- SORT_MODIFIED_DESC("MODIFIED DESC"),
- SORT_LEXICOGRAPHICAL_ASC("TITLE COLLATE NOCASE ASC");
+ SORT_MODIFIED_DESC(0, "MODIFIED DESC"),
+ SORT_LEXICOGRAPHICAL_ASC(1, "TITLE COLLATE NOCASE ASC");
- private String sorder; // sorting method OrderBy for SQL
+ private final int id;
+ private final String title; // sorting method OrderBy for SQL
/***
* Constructor
- * @param orderby given sorting method OrderBy
+ * @param title given sorting method OrderBy
*/
- CategorySortingMethod(String orderby) {
- this.sorder = orderby;
+ CategorySortingMethod(int id, String title) {
+ this.id = id;
+ this.title = title;
}
/***
* Retrieve the sorting method id represented in database
* @return the sorting method id for the enum item
*/
- public int getCSMID() {
- return this.ordinal();
+ public int getId() {
+ return this.id;
}
/***
* Retrieve the sorting method order for SQL
* @return the sorting method order for the enum item
*/
- public String getSorder() {
- return this.sorder;
+ public String getTitle() {
+ return this.title;
}
/***
* Retrieve the corresponding enum value with given the index (ordinal)
- * @param index the index / ordinal of the corresponding enum value stored in DB
+ * @param id the id of the corresponding enum value stored in DB
* @return the corresponding enum item with the index (ordinal)
*/
- public static CategorySortingMethod getCSM(int index) {
- return CategorySortingMethod.values()[index];
+ public static CategorySortingMethod findById(int id) {
+ for (CategorySortingMethod csm : values()) {
+ if (csm.getId() == id) {
+ return csm;
+ }
+ }
+ return SORT_MODIFIED_DESC;
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java
deleted file mode 100644
index e194b11e..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import androidx.annotation.NonNull;
-
-import java.io.Serializable;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-
-import it.niedermann.android.markdown.MarkdownUtil;
-import it.niedermann.owncloud.notes.shared.util.NoteUtil;
-
-/**
- * CloudNote represents a remote note from an OwnCloud server.
- * It can be directly generated from the JSON answer from the server.
- */
-public class CloudNote implements Serializable {
- private long remoteId;
- private String title = "";
- private Calendar modified;
- private String content = "";
- private boolean favorite = false;
- private String category = "";
- private String etag = "";
- private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
-
- public CloudNote(long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag) {
- this.remoteId = remoteId;
- setTitle(title);
- setContent(content);
- setFavorite(favorite);
- setCategory(category);
- setEtag(etag);
- this.modified = modified;
- }
-
- public long getRemoteId() {
- return remoteId;
- }
-
- public void setRemoteId(long remoteId) {
- this.remoteId = remoteId;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = MarkdownUtil.removeMarkdown(title);
- }
-
- public Calendar getModified() {
- return modified;
- }
-
- public String getModified(String format) {
- if (modified == null)
- return null;
- return new SimpleDateFormat(format, Locale.GERMANY).format(this.getModified().getTimeInMillis());
- }
-
- public void setModified(Calendar modified) {
- this.modified = modified;
- }
-
- public String getContent() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public boolean isFavorite() {
- return favorite;
- }
-
- public void setFavorite(boolean favorite) {
- this.favorite = favorite;
- }
-
- public String getEtag() {
- return etag;
- }
-
- public void setEtag(String etag) {
- this.etag = etag;
- }
-
- public String getCategory() {
- return category;
- }
-
- public void setCategory(String category) {
- this.category = category == null ? "" : category;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "R" + getRemoteId() + " " + (isFavorite() ? " (*) " : " ") + getCategory() + " / " + getTitle() + " (" + getModified(DATE_FORMAT) + " / " + getEtag() + ")";
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java
deleted file mode 100644
index 059a1ce3..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import androidx.annotation.NonNull;
-
-import java.io.Serializable;
-import java.util.Calendar;
-
-/**
- * DBNote represents a single note from the local SQLite database with all attributes.
- * It extends CloudNote with attributes required for local data management.
- */
-public class DBNote extends CloudNote implements Item, Serializable {
-
- private final long id;
- private final long accountId;
- private DBStatus status;
- private String excerpt;
- private int scrollY;
-
- public DBNote(long id, long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag, DBStatus status, long accountId, String excerpt, int scrollY) {
- super(remoteId, modified, title, content, favorite, category, etag);
- this.id = id;
- this.excerpt = excerpt;
- this.status = status;
- this.accountId = accountId;
- this.scrollY = scrollY;
- }
-
- public long getId() {
- return id;
- }
-
- public long getAccountId() {
- return accountId;
- }
-
- public DBStatus getStatus() {
- return status;
- }
-
- public void setStatus(DBStatus status) {
- this.status = status;
- }
-
- public String getExcerpt() {
- return excerpt;
- }
-
- public void setExcerpt(String excerpt) {
- this.excerpt = excerpt;
- }
-
- public void setContent(String content) {
- super.setContent(content);
- }
-
- @Override
- public boolean isSection() {
- return false;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "DBNote{" +
- "id=" + id +
- ", accountId=" + accountId +
- ", status=" + status +
- ", excerpt='" + excerpt + '\'' +
- '}';
- }
-
- public int getScrollY() {
- return scrollY;
- }
-
- public void setScrollY(int scrollY) {
- this.scrollY = scrollY;
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
index 4b7e61e4..ef06a277 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
@@ -1,5 +1,7 @@
package it.niedermann.owncloud.notes.shared.model;
+import androidx.annotation.NonNull;
+
/**
* Helps to distinguish between different local change types for Server Synchronization.
* Created by stefan on 19.09.15.
@@ -24,27 +26,15 @@ public enum DBStatus {
*/
LOCAL_DELETED("LOCAL_DELETED");
+ @NonNull
private final String title;
+ @NonNull
public String getTitle() {
return title;
}
- DBStatus(String title) {
+ DBStatus(@NonNull String title) {
this.title = title;
}
-
- /**
- * Parse a String an get the appropriate DBStatus enum element.
- *
- * @param str The String containing the DBStatus identifier. Must not null.
- * @return The DBStatus fitting to the String.
- */
- public static DBStatus parse(String str) {
- if (str.isEmpty()) {
- return DBStatus.VOID;
- } else {
- return DBStatus.valueOf(str);
- }
- }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ENavigationCategoryType.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ENavigationCategoryType.java
new file mode 100644
index 00000000..871a12d4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ENavigationCategoryType.java
@@ -0,0 +1,10 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import java.io.Serializable;
+
+public enum ENavigationCategoryType implements Serializable {
+ RECENT,
+ FAVORITES,
+ UNCATEGORIZED,
+ DEFAULT_CATEGORY
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java
new file mode 100644
index 00000000..2c329727
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import androidx.annotation.NonNull;
+
+public interface IResponseCallback {
+ void onSuccess();
+
+ void onError(@NonNull Throwable t);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java
index 362bc390..234acb91 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java
@@ -1,5 +1,7 @@
package it.niedermann.owncloud.notes.shared.model;
public interface Item {
- boolean isSection();
+ default boolean isSection() {
+ return false;
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java
deleted file mode 100644
index aaafd750..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.NoSuchElementException;
-
-import it.niedermann.owncloud.notes.persistence.NotesClient;
-
-public class LocalAccount {
-
- private long id;
- private String userName;
- private String accountName;
- private String url;
- private String etag;
- private String capabilitiesETag;
- private long modified;
- @Nullable
- private ApiVersion preferredApiVersion;
- @ColorInt
- private int color;
- @ColorInt
- private int textColor;
-
- public long getId() {
- return id;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-
- public String getUserName() {
- return userName;
- }
-
- public void setUserName(String userName) {
- this.userName = userName;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- public String getEtag() {
- return etag;
- }
-
- public void setETag(String etag) {
- this.etag = etag;
- }
-
- public String getAccountName() {
- return accountName;
- }
-
- public void setAccountName(String accountName) {
- this.accountName = accountName;
- }
-
- public long getModified() {
- return modified;
- }
-
- public void setModified(long modified) {
- this.modified = modified;
- }
-
- @Nullable
- public ApiVersion getPreferredApiVersion() {
- return preferredApiVersion;
- }
-
- public String getCapabilitiesETag() {
- return capabilitiesETag;
- }
-
- public void setCapabilitiesETag(String capabilitiesETag) {
- this.capabilitiesETag = capabilitiesETag;
- }
-
- /**
- * @param availableApiVersions <code>["0.2", "1.0", ...]</code>
- */
- public void setPreferredApiVersion(@Nullable String availableApiVersions) {
- // TODO move this logic to NotesClient?
- try {
- if (availableApiVersions == null) {
- this.preferredApiVersion = null;
- return;
- }
- JSONArray versionsArray = new JSONArray(availableApiVersions);
- Collection<ApiVersion> supportedApiVersions = new HashSet<>(versionsArray.length());
- for (int i = 0; i < versionsArray.length(); i++) {
- ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i));
- for (ApiVersion temp : NotesClient.SUPPORTED_API_VERSIONS) {
- if (temp.compareTo(parsedApiVersion) == 0) {
- supportedApiVersions.add(parsedApiVersion);
- break;
- }
- }
- }
- this.preferredApiVersion = Collections.max(supportedApiVersions);
- } catch (JSONException | NoSuchElementException e) {
- e.printStackTrace();
- this.preferredApiVersion = null;
- }
- }
-
- public int getColor() {
- return color;
- }
-
- public void setColor(@ColorInt int color) {
- this.color = color;
- }
-
- public int getTextColor() {
- return textColor;
- }
-
- public void setTextColor(@ColorInt int textColor) {
- this.textColor = textColor;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "LocalAccount{" +
- "id=" + id +
- ", userName='" + userName + '\'' +
- ", accountName='" + accountName + '\'' +
- ", url='" + url + '\'' +
- ", etag='" + etag + '\'' +
- ", modified=" + modified +
- ", preferredApiVersion='" + preferredApiVersion + '\'' +
- ", color=" + color +
- ", textColor=" + textColor +
- ", capabilitiesETag=" + capabilitiesETag +
- '}';
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NavigationCategory.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NavigationCategory.java
new file mode 100644
index 00000000..52a3a117
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NavigationCategory.java
@@ -0,0 +1,76 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.Serializable;
+
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY;
+
+public class NavigationCategory implements Serializable {
+
+ @NonNull
+ private final ENavigationCategoryType type;
+ @Nullable
+ private final String category;
+ private final long accountId;
+
+ public NavigationCategory(@NonNull ENavigationCategoryType type) {
+ if (type == DEFAULT_CATEGORY) {
+ throw new IllegalArgumentException("If you want to provide a " + DEFAULT_CATEGORY + ", call the constructor with an accountId and category as arguments");
+ }
+ this.type = type;
+ this.category = null;
+ this.accountId = Long.MIN_VALUE;
+ }
+
+ public NavigationCategory(long accountId, @Nullable String category) {
+ this.type = DEFAULT_CATEGORY;
+ this.category = category;
+ this.accountId = accountId;
+ }
+
+ @NonNull
+ public ENavigationCategoryType getType() {
+ return type;
+ }
+
+ public long getAccountId() {
+ return accountId;
+ }
+
+ @Nullable
+ public String getCategory() {
+ return category;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof NavigationCategory)) return false;
+
+ NavigationCategory that = (NavigationCategory) o;
+
+ if (accountId != that.accountId) return false;
+ if (type != that.type) return false;
+ return category != null ? category.equals(that.category) : that.category == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type.hashCode();
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (int) (accountId ^ (accountId >>> 32));
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "NavigationCategory{" +
+ "type=" + type +
+ ", category='" + category + '\'' +
+ ", accountId=" + accountId +
+ '}';
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java
index 7194752c..e34c005b 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java
@@ -6,6 +6,4 @@ public interface NoteClickListener {
void onNoteClick(int position, View v);
void onNoteFavoriteClick(int position, View v);
-
- boolean onNoteLongClick(int position, View v);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java
index 9f34b907..dca53fb9 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java
@@ -8,10 +8,10 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Calendar;
-import java.util.GregorianCalendar;
import java.util.List;
import it.niedermann.owncloud.notes.persistence.NotesClient;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
/**
* Provides entity classes for handling server responses with a single note ({@link NoteResponse}) or a list of notes ({@link NotesResponse}).
@@ -23,7 +23,7 @@ public class ServerResponse {
super(response);
}
- public CloudNote getNote() throws JSONException {
+ public Note getNote() throws JSONException {
return getNoteFromJSON(new JSONObject(getContent()));
}
}
@@ -33,8 +33,8 @@ public class ServerResponse {
super(response);
}
- public List<CloudNote> getNotes() throws JSONException {
- List<CloudNote> notesList = new ArrayList<>();
+ public List<Note> getNotes() throws JSONException {
+ List<Note> notesList = new ArrayList<>();
JSONArray notes = new JSONArray(getContent());
for (int i = 0; i < notes.length(); i++) {
JSONObject json = notes.getJSONObject(i);
@@ -59,7 +59,7 @@ public class ServerResponse {
return response.getETag();
}
- public long getLastModified() {
+ public Calendar getLastModified() {
return response.getLastModified();
}
@@ -68,16 +68,16 @@ public class ServerResponse {
return response.getSupportedApiVersions();
}
- CloudNote getNoteFromJSON(JSONObject json) throws JSONException {
- long id = 0;
+ Note getNoteFromJSON(JSONObject json) throws JSONException {
+ long remoteId = 0;
String title = "";
String content = "";
Calendar modified = null;
boolean favorite = false;
- String category = null;
+ String category = "";
String etag = null;
if (!json.isNull(NotesClient.JSON_ID)) {
- id = json.getLong(NotesClient.JSON_ID);
+ remoteId = json.getLong(NotesClient.JSON_ID);
}
if (!json.isNull(NotesClient.JSON_TITLE)) {
title = json.getString(NotesClient.JSON_TITLE);
@@ -86,8 +86,8 @@ public class ServerResponse {
content = json.getString(NotesClient.JSON_CONTENT);
}
if (!json.isNull(NotesClient.JSON_MODIFIED)) {
- modified = GregorianCalendar.getInstance();
- modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1000);
+ modified = Calendar.getInstance();
+ modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1_000);
}
if (!json.isNull(NotesClient.JSON_FAVORITE)) {
favorite = json.getBoolean(NotesClient.JSON_FAVORITE);
@@ -98,6 +98,6 @@ public class ServerResponse {
if (!json.isNull(NotesClient.JSON_ETAG)) {
etag = json.getString(NotesClient.JSON_ETAG);
}
- return new CloudNote(id, modified, title, content, favorite, category, etag);
+ return new Note(remoteId, modified, title, content, category, favorite, etag);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java
index 235711ef..8506c713 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java
@@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteDatabase;
public class DatabaseIndexUtil {
@@ -14,20 +15,20 @@ public class DatabaseIndexUtil {
}
- public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String ...columns) {
- for (String column: columns) {
+ public static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String... columns) {
+ for (String column : columns) {
createIndex(db, table, column);
}
}
- public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) {
+ public static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String column) {
String indexName = table + "_" + column + "_idx";
Log.v(TAG, "Creating database index: CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")");
- db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")");
+ db.execSQL("CREATE INDEX " + indexName + " ON " + table + "(" + column + ")");
}
- public static void dropIndexes(@NonNull SQLiteDatabase db) {
- try (Cursor c = db.query("sqlite_master", new String[]{"name", "sql"}, "type=?", new String[]{"index"}, null, null, null)) {
+ public static void dropIndexes(@NonNull SupportSQLiteDatabase db) {
+ try (Cursor c = db.query("SELECT name, sql FROM sqlite_master WHERE type = 'index'")) {
while (c.moveToNext()) {
// Skip automatic indexes which we can't drop manually
if (c.getString(1) != null) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java
new file mode 100644
index 00000000..ad6b6793
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java
@@ -0,0 +1,55 @@
+package it.niedermann.owncloud.notes.shared.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.MetricAffectingSpan;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.owncloud.notes.NotesApplication;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+
+public class DisplayUtils {
+
+ private DisplayUtils() {
+
+ }
+
+ public static List<NavigationItem.CategoryNavigationItem> convertToCategoryNavigationItem(@NonNull Context context, @NonNull Collection<CategoryWithNotesCount> counter) {
+ return counter.stream()
+ .map(ctr -> convertToCategoryNavigationItem(context, ctr))
+ .collect(Collectors.toList());
+ }
+
+ public static NavigationItem.CategoryNavigationItem convertToCategoryNavigationItem(@NonNull Context context, @NonNull CategoryWithNotesCount counter) {
+ Resources res = context.getResources();
+ String category = counter.getCategory().toLowerCase();
+ int icon = NavigationAdapter.ICON_FOLDER;
+ if (category.equals(res.getString(R.string.category_music).toLowerCase())) {
+ icon = R.drawable.ic_library_music_grey600_24dp;
+ } else if (category.equals(res.getString(R.string.category_movies).toLowerCase()) || category.equals(res.getString(R.string.category_movie).toLowerCase())) {
+ icon = R.drawable.ic_local_movies_grey600_24dp;
+ } else if (category.equals(res.getString(R.string.category_work).toLowerCase())) {
+ icon = R.drawable.ic_work_grey600_24dp;
+ }
+ return new NavigationItem.CategoryNavigationItem("category:" + counter.getCategory(), counter.getCategory(), counter.getTotalNotes(), icon, counter.getAccountId(), counter.getCategory());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
index 456b85bb..e5b8afae 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
@@ -65,7 +65,7 @@ public class NoteUtil {
@NonNull
public static String generateNoteExcerpt(@NonNull String content, @Nullable String title) {
content = removeMarkdown(replaceCheckboxesWithEmojis(content.trim()));
- if(TextUtils.isEmpty(content)) {
+ if (TextUtils.isEmpty(content)) {
return "";
}
if (!TextUtils.isEmpty(title)) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java
index 019a2153..f561f638 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java
@@ -1,27 +1,32 @@
package it.niedermann.owncloud.notes.widget;
+import androidx.annotation.IntRange;
+import androidx.room.PrimaryKey;
+
public abstract class AbstractWidgetData {
- private int appWidgetId;
+ @PrimaryKey
+ private int id;
private long accountId;
+ @IntRange(from = 0, to = 2)
private int themeMode;
protected AbstractWidgetData() {
-
+ // Default constructor
}
- protected AbstractWidgetData(int appWidgetId, long accountId, int themeMode) {
- this.appWidgetId = appWidgetId;
+ protected AbstractWidgetData(int id, long accountId, @IntRange(from = 0, to = 2) int themeMode) {
+ this.id = id;
this.accountId = accountId;
this.themeMode = themeMode;
}
- public int getAppWidgetId() {
- return appWidgetId;
+ public int getId() {
+ return id;
}
- public void setAppWidgetId(int appWidgetId) {
- this.appWidgetId = appWidgetId;
+ public void setId(int id) {
+ this.id = id;
}
public long getAccountId() {
@@ -32,11 +37,32 @@ public abstract class AbstractWidgetData {
this.accountId = accountId;
}
+ @IntRange(from = 0, to = 2)
public int getThemeMode() {
return themeMode;
}
- public void setThemeMode(int themeMode) {
+ public void setThemeMode(@IntRange(from = 0, to = 2) int themeMode) {
this.themeMode = themeMode;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AbstractWidgetData)) return false;
+
+ AbstractWidgetData that = (AbstractWidgetData) o;
+
+ if (id != that.id) return false;
+ if (accountId != that.accountId) return false;
+ return themeMode == that.themeMode;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + (int) (accountId ^ (accountId >>> 32));
+ result = 31 * result + themeMode;
+ return result;
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java
new file mode 100644
index 00000000..ad3830a4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java
@@ -0,0 +1,68 @@
+package it.niedermann.owncloud.notes.widget.notelist;
+
+import android.app.Application;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.main.MainActivity;
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
+import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem;
+
+public class NoteListViewModel extends AndroidViewModel {
+
+ private static final String TAG = NoteListViewModel.class.getSimpleName();
+
+ @NonNull
+ private final NotesDatabase db;
+
+ public NoteListViewModel(@NonNull Application application) {
+ super(application);
+ this.db = NotesDatabase.getInstance(application);
+ }
+
+ public LiveData<List<NavigationItem>> getAdapterCategories(Long accountId) {
+ return distinctUntilChanged(
+ switchMap(distinctUntilChanged(db.getNoteDao().count$(accountId)), (count) -> {
+ Log.v(TAG, "[getAdapterCategories] countLiveData: " + count);
+ return switchMap(distinctUntilChanged(db.getNoteDao().countFavorites$(accountId)), (favoritesCount) -> {
+ Log.v(TAG, "[getAdapterCategories] getFavoritesCountLiveData: " + favoritesCount);
+ return map(distinctUntilChanged(db.getNoteDao().getCategories$(accountId)), fromDatabase -> {
+ final List<NavigationItem.CategoryNavigationItem> categories = convertToCategoryNavigationItem(getApplication(), fromDatabase);
+
+ final List<NavigationItem> items = new ArrayList<>(fromDatabase.size() + 3);
+ items.add(new NavigationItem(MainActivity.ADAPTER_KEY_RECENT, getApplication().getString(R.string.label_all_notes), count, R.drawable.ic_access_time_grey600_24dp, RECENT));
+ items.add(new NavigationItem(MainActivity.ADAPTER_KEY_STARRED, getApplication().getString(R.string.label_favorites), favoritesCount, R.drawable.ic_star_yellow_24dp, FAVORITES));
+
+ if (categories.size() > 2 && categories.get(2).label.isEmpty()) {
+ items.add(new NavigationItem(MainActivity.ADAPTER_KEY_UNCATEGORIZED, "", null, NavigationAdapter.ICON_NOFOLDER));
+ }
+
+ for (NavigationItem item : categories) {
+ final int slashIndex = item.label.indexOf('/');
+
+ item.label = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
+ item.id = "category:" + item.label;
+ items.add(item);
+ }
+ return items;
+ });
+ });
+ })
+ );
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java
index cbed8b79..98ebf2fe 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java
@@ -13,8 +13,8 @@ import android.widget.RemoteViews;
import java.util.NoSuchElementException;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
public class NoteListWidget extends AppWidgetProvider {
private static final String TAG = NoteListWidget.class.getSimpleName();
@@ -26,7 +26,7 @@ public class NoteListWidget extends AppWidgetProvider {
for (int appWidgetId : appWidgetIds) {
try {
- final NoteListsWidgetData data = db.getNoteListWidgetData(appWidgetId);
+ final NotesListWidgetData data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId);
final Intent serviceIntent = new Intent(context, NoteListWidgetService.class);
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
@@ -83,7 +83,7 @@ public class NoteListWidget extends AppWidgetProvider {
final NotesDatabase db = NotesDatabase.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- db.removeNoteListWidget(appWidgetId);
+ new Thread(() -> db.getWidgetNotesListDao().removeNoteListWidget(appWidgetId)).start();
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java
index e927c3b1..2c2aa086 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java
@@ -3,62 +3,54 @@ package it.niedermann.owncloud.notes.widget.notelist;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.lifecycle.ViewModelProvider;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.LockedActivity;
-import it.niedermann.owncloud.notes.main.MainActivity;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
-import it.niedermann.owncloud.notes.main.NavigationAdapter;
-import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
import it.niedermann.owncloud.notes.NotesApplication;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ActivityNoteListConfigurationBinding;
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
+import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryOptions;
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_ALL;
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_CATEGORY;
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_STARRED;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
+
public class NoteListWidgetConfigurationActivity extends LockedActivity {
private static final String TAG = Activity.class.getSimpleName();
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+ private Account localAccount = null;
- private LocalAccount localAccount = null;
-
+ private ActivityNoteListConfigurationBinding binding;
+ private NoteListViewModel viewModel;
private NavigationAdapter adapterCategories;
- private NavigationAdapter.NavigationItem itemRecent;
- private NavigationAdapter.NavigationItem itemFavorites;
private NotesDatabase db = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
- setContentView(R.layout.activity_note_list_configuration);
db = NotesDatabase.getInstance(this);
- try {
- this.localAccount = db.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name);
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- e.printStackTrace();
- Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show();
- // TODO Present user with app login screen
- Log.w(TAG, "onCreate: user not logged in");
- finish();
- return;
- }
final Bundle extras = getIntent().getExtras();
if (extras != null) {
@@ -71,111 +63,82 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
finish();
}
- itemRecent = new NavigationAdapter.NavigationItem(MainActivity.ADAPTER_KEY_RECENT,
- getString(R.string.label_all_notes),
- null,
- R.drawable.ic_access_time_grey600_24dp);
- itemFavorites = new NavigationAdapter.NavigationItem(MainActivity.ADAPTER_KEY_STARRED,
- getString(R.string.label_favorites),
- null,
- R.drawable.ic_star_yellow_24dp);
- RecyclerView recyclerView;
- RecyclerView.LayoutManager layoutManager;
-
- adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() {
+ viewModel = new ViewModelProvider(this).get(NoteListViewModel.class);
+ binding = ActivityNoteListConfigurationBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ adapterCategories = new NavigationAdapter(this, new NavigationClickListener() {
@Override
- public void onItemClick(NavigationAdapter.NavigationItem item) {
- NoteListsWidgetData data = new NoteListsWidgetData();
-
- data.setAppWidgetId(appWidgetId);
- if (itemRecent.equals(item)) {
- data.setMode(NoteListsWidgetData.MODE_DISPLAY_ALL);
- } else if (itemFavorites.equals(item)) {
- data.setMode(NoteListsWidgetData.MODE_DISPLAY_STARRED);
- } else {
- data.setMode(NoteListsWidgetData.MODE_DISPLAY_CATEGORY);
- if (item instanceof CategoryNavigationItem) {
- data.setCategoryId(((CategoryNavigationItem) item).categoryId);
- } else {
- throw new IllegalStateException("Tried to choose a category, but ");
+ public void onItemClick(NavigationItem item) {
+ NotesListWidgetData data = new NotesListWidgetData();
+
+ data.setId(appWidgetId);
+ if (item.type != null) {
+ switch (item.type) {
+ case RECENT: {
+ data.setMode(MODE_DISPLAY_ALL);
+ break;
+ }
+ case FAVORITES: {
+ data.setMode(MODE_DISPLAY_STARRED);
+ break;
+ }
+ case UNCATEGORIZED: {
+ data.setMode(MODE_DISPLAY_CATEGORY);
+ data.setCategory(null);
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ if (item.getClass() == NavigationItem.CategoryNavigationItem.class) {
+ data.setMode(MODE_DISPLAY_CATEGORY);
+ data.setCategory(((NavigationItem.CategoryNavigationItem) item).category);
+ } else {
+ data.setMode(MODE_DISPLAY_ALL);
+ Log.e(TAG, "Unknown item navigation type. Fallback to show " + RECENT);
+ }
+ }
}
+ } else {
+ data.setMode(MODE_DISPLAY_ALL);
+ Log.e(TAG, "Unknown item navigation type. Fallback to show " + RECENT);
}
data.setAccountId(localAccount.getId());
data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId());
- db.createOrUpdateNoteListWidgetData(data);
+ new Thread(() -> {
+ db.getWidgetNotesListDao().createOrUpdateNoteListWidgetData(data);
- Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
- getApplicationContext(), NoteListWidget.class);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- setResult(RESULT_OK, updateIntent);
- getApplicationContext().sendBroadcast(updateIntent);
- finish();
+ final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), NoteListWidget.class)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ setResult(RESULT_OK, updateIntent);
+ getApplicationContext().sendBroadcast(updateIntent);
+ finish();
+ }).start();
}
- public void onIconClick(NavigationAdapter.NavigationItem item) {
+ public void onIconClick(NavigationItem item) {
onItemClick(item);
}
});
- recyclerView = findViewById(R.id.recycler_view);
- recyclerView.setHasFixedSize(true);
- layoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(adapterCategories);
- }
+ binding.recyclerView.setAdapter(adapterCategories);
- @Override
- protected void onResume() {
- super.onResume();
- new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ new Thread(() -> {
+ try {
+ this.localAccount = db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
+ Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show();
+ // TODO Present user with app login screen
+ Log.w(TAG, "onCreate: user not logged in");
+ finish();
+ }
+ runOnUiThread(() -> viewModel.getAdapterCategories(localAccount.getId()).observe(this, (navigationItems) -> adapterCategories.setItems(navigationItems)));
+ }).start();
}
@Override
public void applyBrand(int mainColor, int textColor) {
}
-
- private class LoadCategoryListTask extends AsyncTask<Void, Void, List<NavigationAdapter.NavigationItem>> {
- @Override
- protected List<NavigationAdapter.NavigationItem> doInBackground(Void... voids) {
- if (localAccount == null) {
- return new ArrayList<>();
- }
- NavigationAdapter.NavigationItem itemUncategorized;
- List<CategoryNavigationItem> categories = db.getCategories(localAccount.getId());
-
- if (!categories.isEmpty() && categories.get(0).label.isEmpty()) {
- itemUncategorized = categories.get(0);
- itemUncategorized.label = getString(R.string.action_uncategorized);
- itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER;
- }
-
- Map<String, Integer> favorites = db.getFavoritesCount(localAccount.getId());
- //noinspection ConstantConditions
- int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0;
- //noinspection ConstantConditions
- int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0;
- itemFavorites.count = numFavorites;
- itemRecent.count = numFavorites + numNonFavorites;
-
- ArrayList<NavigationAdapter.NavigationItem> items = new ArrayList<>();
- items.add(itemRecent);
- items.add(itemFavorites);
-
- for (NavigationAdapter.NavigationItem item : categories) {
- int slashIndex = item.label.indexOf('/');
-
- item.label = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
- item.id = "category:" + item.label;
- items.add(item);
- }
- return items;
- }
-
- @Override
- protected void onPostExecute(List<NavigationAdapter.NavigationItem> items) {
- adapterCategories.setItems(items);
- }
- }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java
index 3e1fdfd0..7917b25a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java
@@ -20,31 +20,32 @@ import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.main.MainActivity;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.Category;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.shared.model.LocalAccount;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.util.NotesColorUtil;
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.PARAM_CATEGORY;
-import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_ALL;
-import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_CATEGORY;
-import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_STARRED;
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_ALL;
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_CATEGORY;
+import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_STARRED;
public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
private static final String TAG = NoteListWidgetFactory.class.getSimpleName();
private final Context context;
- private final NoteListsWidgetData data;
+ private final int appWidgetId;
private final NotesDatabase db;
- private final List<DBNote> dbNotes = new ArrayList<>();
+ @NonNull
+ private final List<Note> dbNotes = new ArrayList<>();
+ private NotesListWidgetData data;
NoteListWidgetFactory(Context context, Intent intent) {
this.context = context;
- final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID);
-
+ this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
db = NotesDatabase.getInstance(context);
- data = db.getNoteListWidgetData(appWidgetId);
}
@Override
@@ -56,17 +57,21 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
public void onDataSetChanged() {
dbNotes.clear();
try {
+ data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId);
Log.v(TAG, "--- data - " + data);
switch (data.getMode()) {
case MODE_DISPLAY_ALL:
- dbNotes.addAll(db.getNotes(data.getAccountId()));
+ dbNotes.addAll(db.getNoteDao().searchRecentByModified(data.getAccountId(), "%"));
break;
case MODE_DISPLAY_STARRED:
- dbNotes.addAll(db.searchNotes(data.getAccountId(), null, null, true));
+ dbNotes.addAll(db.getNoteDao().searchFavoritesByModified(data.getAccountId(), "%"));
break;
case MODE_DISPLAY_CATEGORY:
- if (data.getCategoryId() != null) {
- dbNotes.addAll(db.searchNotes(data.getAccountId(), null, db.getCategoryTitleById(data.getAccountId(), data.getCategoryId()), null));
+ default:
+ if (data.getCategory() != null) {
+ dbNotes.addAll(db.getNoteDao().searchCategoryByModified(data.getAccountId(), "%", data.getCategory()));
+ } else {
+ dbNotes.addAll(db.getNoteDao().searchUncategorizedByModified(data.getAccountId(), "%"));
}
break;
}
@@ -80,11 +85,6 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
//NoOp
}
- /**
- * getCount()
- *
- * @return Total number of entries
- */
@Override
public int getCount() {
return dbNotes.size() + 1;
@@ -95,18 +95,12 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
final RemoteViews note_content;
if (position == 0) {
- final LocalAccount localAccount = db.getAccount(data.getAccountId());
+ final Account localAccount = db.getAccountDao().getAccountById(data.getAccountId());
final Intent openIntent = new Intent(Intent.ACTION_MAIN).setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName()));
final Intent createIntent = new Intent(context, EditNoteActivity.class);
final Bundle extras = new Bundle();
- String category = null;
- Long categoryId = data.getCategoryId();
- if (categoryId != null) {
- category = db.getCategoryTitleById(data.getAccountId(), categoryId);
- }
-
- extras.putSerializable(PARAM_CATEGORY, new Category(category, data.getMode() == MODE_DISPLAY_STARRED));
+ extras.putSerializable(PARAM_CATEGORY, data.getMode() == MODE_DISPLAY_STARRED ? new NavigationCategory(ENavigationCategoryType.FAVORITES) : new NavigationCategory(localAccount.getId(), data.getCategory()));
extras.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, data.getAccountId());
createIntent.putExtras(extras);
@@ -115,7 +109,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry_add);
note_content.setOnClickFillInIntent(R.id.widget_entry_content_tv, openIntent);
note_content.setOnClickFillInIntent(R.id.widget_entry_fav_icon, createIntent);
- note_content.setTextViewText(R.id.widget_entry_content_tv, getCategoryTitle(context, data.getMode(), category));
+ note_content.setTextViewText(R.id.widget_entry_content_tv, getCategoryTitle(context, data.getMode(), data.getCategory()));
note_content.setImageViewResource(R.id.widget_entry_fav_icon, R.drawable.ic_add_blue_24dp);
note_content.setInt(R.id.widget_entry_fav_icon, "setColorFilter", NotesColorUtil.contrastRatioIsSufficient(ContextCompat.getColor(context, R.color.widget_background), localAccount.getColor())
? localAccount.getColor()
@@ -127,7 +121,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
return null;
}
- final DBNote note = dbNotes.get(position);
+ final Note note = dbNotes.get(position);
final Intent fillInIntent = new Intent(context, EditNoteActivity.class);
final Bundle extras = new Bundle();
extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId());
@@ -139,7 +133,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry);
note_content.setOnClickFillInIntent(R.id.widget_note_list_entry, fillInIntent);
note_content.setTextViewText(R.id.widget_entry_content_tv, note.getTitle());
- note_content.setImageViewResource(R.id.widget_entry_fav_icon, note.isFavorite()
+ note_content.setImageViewResource(R.id.widget_entry_fav_icon, note.getFavorite()
? R.drawable.ic_star_yellow_24dp
: R.drawable.ic_star_grey_ccc_24dp);
}
@@ -183,7 +177,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
Log.e(TAG, "Could not find position \"" + position + "\" in dbNotes list.");
return -2;
}
- DBNote note = dbNotes.get(position);
+ Note note = dbNotes.get(position);
return note.getId();
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java
deleted file mode 100644
index a21714ce..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package it.niedermann.owncloud.notes.widget.notelist;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import it.niedermann.owncloud.notes.widget.AbstractWidgetData;
-
-public class NoteListsWidgetData extends AbstractWidgetData {
- public static final int MODE_DISPLAY_ALL = 0;
- public static final int MODE_DISPLAY_STARRED = 1;
- public static final int MODE_DISPLAY_CATEGORY = 2;
-
- @IntRange(from = 0, to = 2)
- private int mode;
- @Nullable
- private Long categoryId;
-
- public int getMode() {
- return mode;
- }
-
- public void setMode(@IntRange(from = 0, to = 2) int mode) {
- this.mode = mode;
- }
-
- @Nullable
- public Long getCategoryId() {
- return categoryId;
- }
-
- public void setCategoryId(@Nullable Long categoryId) {
- this.categoryId = categoryId;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "NoteListsWidgetData{" +
- "mode=" + mode +
- ", categoryId=" + categoryId +
- '}';
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java
index c82ba9a5..3ad56c76 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java
@@ -10,12 +10,11 @@ import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;
-import java.util.NoSuchElementException;
-
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.edit.BaseNoteFragment;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
public class SingleNoteWidget extends AppWidgetProvider {
@@ -26,9 +25,8 @@ public class SingleNoteWidget extends AppWidgetProvider {
final NotesDatabase db = NotesDatabase.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- try {
- final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId);
-
+ final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId);
+ if(data != null) {
templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, data.getAccountId());
final PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent,
@@ -45,7 +43,7 @@ public class SingleNoteWidget extends AppWidgetProvider {
awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv);
awm.updateAppWidget(appWidgetId, views);
- } catch (NoSuchElementException e) {
+ } else {
Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget");
}
}
@@ -71,7 +69,7 @@ public class SingleNoteWidget extends AppWidgetProvider {
final NotesDatabase db = NotesDatabase.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- db.removeSingleNoteWidget(appWidgetId);
+ new Thread(() -> db.getWidgetSingleNoteDao().removeSingleNoteWidget(appWidgetId)).start();
}
super.onDeleted(context, appWidgetIds);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java
index ef3bbbe6..eb06201a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java
@@ -12,11 +12,12 @@ import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.exception.ExceptionHandler;
import it.niedermann.owncloud.notes.main.MainActivity;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
-import it.niedermann.owncloud.notes.NotesApplication;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
public class SingleNoteWidgetConfigurationActivity extends MainActivity {
@@ -41,7 +42,7 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity {
@Override
public void onNoteClick(int position, View v) {
- final DBNote note = (DBNote) adapter.getItem(position);
+ final Note note = (Note) adapter.getItem(position);
final Bundle extras = getIntent().getExtras();
if (extras == null) {
@@ -51,24 +52,25 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- try {
- db.createOrUpdateSingleNoteWidgetData(
- new SingleNoteWidgetData(
- appWidgetId,
- note.getAccountId(),
- note.getId(),
- NotesApplication.getAppTheme(this).getModeId()
- )
- );
- final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
- getApplicationContext(), SingleNoteWidget.class)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- setResult(RESULT_OK, updateIntent);
- getApplicationContext().sendBroadcast(updateIntent);
- } catch (SQLException e) {
- Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
- }
-
- finish();
+ new Thread(() -> {
+ try {
+ mainViewModel.createOrUpdateSingleNoteWidgetData(
+ new SingleNoteWidgetData(
+ appWidgetId,
+ note.getAccountId(),
+ note.getId(),
+ NotesApplication.getAppTheme(this).getModeId()
+ )
+ );
+ final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
+ getApplicationContext(), SingleNoteWidget.class)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ setResult(RESULT_OK, updateIntent);
+ getApplicationContext().sendBroadcast(updateIntent);
+ finish();
+ } catch (SQLException e) {
+ Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
+ }
+ }).start();
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java
deleted file mode 100644
index 79b83006..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package it.niedermann.owncloud.notes.widget.singlenote;
-
-import it.niedermann.owncloud.notes.widget.AbstractWidgetData;
-
-public class SingleNoteWidgetData extends AbstractWidgetData {
- private long noteId;
-
- public SingleNoteWidgetData() {
-
- }
-
- public SingleNoteWidgetData(int appWidgetId, long accountId, long noteId, int themeMode) {
- super(appWidgetId, accountId, themeMode);
- this.noteId = noteId;
- }
-
- public long getNoteId() {
- return noteId;
- }
-
- public void setNoteId(long noteId) {
- this.noteId = noteId;
- }
-
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java
index 5c7ddd9e..db26da24 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java
@@ -10,13 +10,12 @@ import android.widget.RemoteViewsService;
import androidx.annotation.Nullable;
-import java.util.NoSuchElementException;
-
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
-import it.niedermann.owncloud.notes.shared.model.DBNote;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
@@ -25,14 +24,13 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa
private final NotesDatabase db;
@Nullable
- private DBNote note;
+ private Note note;
private static final String TAG = SingleNoteWidget.class.getSimpleName();
SingleNoteWidgetFactory(Context context, Intent intent) {
this.context = context;
- this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID);
+ this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
this.db = NotesDatabase.getInstance(context);
}
@@ -43,16 +41,16 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa
@Override
public void onDataSetChanged() {
- try {
- final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId);
+ final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId);
+ if(data != null) {
final long noteId = data.getNoteId();
Log.v(TAG, "Fetch note with id " + noteId);
- note = db.getNote(data.getAccountId(), noteId);
+ note = db.getNoteDao().getNoteById(noteId);
if (note == null) {
Log.e(TAG, "Error: note not found");
}
- } catch (NoSuchElementException e) {
+ } else {
Log.w(TAG, "Widget with ID " + appWidgetId + " seems to be not configured yet.");
}
}