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-05-03 18:36:24 +0300
committerStefan Niedermann <info@niedermann.it>2021-05-03 19:22:38 +0300
commitbbf569b0e9a1a93e4fff8074cc180274fc1f2d24 (patch)
treee43c3237b7a36d8d3f4881129cd977e93690f9db
parente3c4c1cb40ba229f50dd534c9edef42cb807ed18 (diff)
parent1cc44ede8e6c605345d794452c099a07a49d2f49 (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/ManageAccountsActivity.java # app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java # app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java # app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java # app/src/main/res/values/strings.xml
-rw-r--r--.github/dependabot.yml7
-rw-r--r--app/build.gradle22
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java36
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java73
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java11
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java89
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java131
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java11
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java236
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java74
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java137
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java78
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java15
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java228
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java63
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java78
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java466
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java930
-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.java148
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/SSOClient.java82
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java14
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java24
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java81
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java142
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java36
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java43
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java18
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java27
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java32
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/NotesSettings.java34
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java103
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerSettings.java47
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java7
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java18
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java14
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java12
-rw-r--r--app/src/main/res/layout/activity_import_account.xml13
-rw-r--r--app/src/main/res/values-ca/strings.xml17
-rw-r--r--app/src/main/res/values-cs-rCZ/strings.xml12
-rw-r--r--app/src/main/res/values-da/strings.xml2
-rw-r--r--app/src/main/res/values-de/strings.xml10
-rw-r--r--app/src/main/res/values-el/strings.xml2
-rw-r--r--app/src/main/res/values-es/strings.xml16
-rw-r--r--app/src/main/res/values-eu/strings.xml13
-rw-r--r--app/src/main/res/values-fa/strings.xml2
-rw-r--r--app/src/main/res/values-fr/strings.xml2
-rw-r--r--app/src/main/res/values-gl/strings.xml5
-rw-r--r--app/src/main/res/values-he/strings.xml2
-rw-r--r--app/src/main/res/values-hr/strings.xml2
-rw-r--r--app/src/main/res/values-hu-rHU/strings.xml27
-rw-r--r--app/src/main/res/values-it/strings.xml4
-rw-r--r--app/src/main/res/values-ko/strings.xml2
-rw-r--r--app/src/main/res/values-nl/strings.xml2
-rw-r--r--app/src/main/res/values-pl/strings.xml12
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml10
-rw-r--r--app/src/main/res/values-ru/strings.xml22
-rw-r--r--app/src/main/res/values-sc/strings.xml4
-rw-r--r--app/src/main/res/values-sl/strings.xml18
-rw-r--r--app/src/main/res/values-tr/strings.xml10
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml9
-rw-r--r--app/src/main/res/values-zh-rHK/strings.xml9
-rw-r--r--app/src/main/res/values/strings.xml10
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java33
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java4
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTest.java149
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java187
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java8
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java5
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004000.txt1
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004001.txt5
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004002.txt2
-rw-r--r--markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java132
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java56
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java3
87 files changed, 2773 insertions, 2070 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..34477c7e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: gradle
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/app/build.gradle b/app/build.gradle
index 569d2934..3dcaf300 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,8 +14,8 @@ android {
applicationId "it.niedermann.owncloud.notes"
minSdkVersion 21
targetSdkVersion 29
- versionCode 3003001
- versionName "3.3.1"
+ versionCode 3004001
+ versionName "3.4.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
@@ -78,8 +78,8 @@ dependencies {
implementation 'com.github.stefan-niedermann:android-commons:0.2.0'
// Glide
- implementation 'com.github.bumptech.glide:glide:4.11.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
// Markdown
implementation project(path: ':markdown')
@@ -95,13 +95,23 @@ dependencies {
implementation "com.google.android.material:material:1.3.0"
// Database
- implementation "androidx.room:room-runtime:2.2.6"
- annotationProcessor "androidx.room:room-compiler:2.2.6"
+ implementation "androidx.room:room-runtime:2.3.0"
+ annotationProcessor "androidx.room:room-compiler:2.3.0"
+
+ // Retrofit
+ implementation 'com.squareup.retrofit2:retrofit:2.6.4'
+
+ // Gson
+ implementation 'com.google.code.gson:gson:2.8.6'
+
+ // ReactiveX
+ implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Testing
testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-core:3.9.0'
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'androidx.test.ext:junit:1.1.2'
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 79467d86..f5643d86 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
@@ -21,7 +21,7 @@ import it.niedermann.owncloud.notes.R;
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.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.applyBrandToLayerDrawable;
@@ -33,7 +33,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
private static final String KEY_CURRENT_ACCOUNT_ID = "current_account_id";
- private NotesDatabase db;
+ private NotesRepository repo;
private DialogAccountSwitcherBinding binding;
private AccountSwitcherListener accountSwitcherListener;
private long currentAccountId;
@@ -55,7 +55,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
this.currentAccountId = args.getLong(KEY_CURRENT_ACCOUNT_ID);
}
- db = NotesDatabase.getInstance(requireActivity());
+ repo = NotesRepository.getInstance(requireContext());
}
@NonNull
@@ -63,7 +63,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
- final LiveData<Account> account$ = db.getAccountDao().getAccountById$(currentAccountId);
+ final LiveData<Account> account$ = repo.getAccountById$(currentAccountId);
account$.observe(requireActivity(), (currentLocalAccount) -> {
account$.removeObservers(requireActivity());
@@ -81,7 +81,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
dismiss();
}));
binding.accountsList.setAdapter(adapter);
- final LiveData<List<Account>> localAccounts$ = db.getAccountDao().getAccounts$();
+ final LiveData<List<Account>> localAccounts$ = repo.getAccounts$();
localAccounts$.observe(requireActivity(), (localAccounts) -> {
localAccounts$.removeObservers(requireActivity());
for (Account localAccount : localAccounts) {
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 383d9e1b..b1cf1d54 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
@@ -41,7 +41,7 @@ import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener;
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.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
@@ -76,7 +76,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
@Nullable
private Note originalNote;
private int originalScrollY;
- protected NotesDatabase db;
+ protected NotesRepository repo;
private NoteFragmentListener listener;
private boolean titleModified = false;
@@ -90,7 +90,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
} catch (ClassCastException e) {
throw new ClassCastException(context.getClass() + " must implement " + NoteFragmentListener.class);
}
- db = NotesDatabase.getInstance(context);
+ repo = NotesRepository.getInstance(context);
}
@Override
@@ -99,7 +99,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
new Thread(() -> {
try {
SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext());
- this.localAccount = db.getAccountDao().getAccountByName(ssoAccount.name);
+ this.localAccount = repo.getAccountByName(ssoAccount.name);
if (savedInstanceState == null) {
long id = requireArguments().getLong(PARAM_NOTE_ID);
@@ -107,11 +107,11 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID);
if (accountId > 0) {
/* Switch account if account id has been provided */
- this.localAccount = db.getAccountDao().getAccountById(accountId);
+ this.localAccount = repo.getAccountById(accountId);
SingleAccountHelper.setCurrentAccount(requireContext().getApplicationContext(), localAccount.getAccountName());
}
isNew = false;
- note = originalNote = db.getNoteDao().getNoteById(id);
+ note = originalNote = repo.getNoteById(id);
requireActivity().runOnUiThread(() -> onNoteLoaded(note));
requireActivity().invalidateOptionsMenu();
} else {
@@ -126,7 +126,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
requireActivity().invalidateOptionsMenu();
}
} else {
- note = db.addNote(localAccount.getId(), cloudNote);
+ note = repo.addNote(localAccount.getId(), cloudNote);
originalNote = null;
requireActivity().runOnUiThread(() -> onNoteLoaded(note));
requireActivity().invalidateOptionsMenu();
@@ -193,7 +193,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
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_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) >= 0);
menu.findItem(R.id.menu_delete).setVisible(!isNew);
}
}
@@ -213,19 +213,19 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
if (itemId == R.id.menu_cancel) {
new Thread(() -> {
if (originalNote == null) {
- db.deleteNoteAndSync(localAccount, note.getId());
+ repo.deleteNoteAndSync(localAccount, note.getId());
} else {
- db.updateNoteAndSync(localAccount, originalNote, null, null, null);
+ repo.updateNoteAndSync(localAccount, originalNote, null, null, null);
}
}).start();
listener.close();
return true;
} else if (itemId == R.id.menu_delete) {
- db.deleteNoteAndSync(localAccount, note.getId());
+ repo.deleteNoteAndSync(localAccount, note.getId());
listener.close();
return true;
} else if (itemId == R.id.menu_favorite) {
- db.toggleFavoriteAndSync(localAccount, note.getId());
+ repo.toggleFavoriteAndSync(localAccount, note.getId());
listener.onNoteUpdated(note);
prepareFavoriteOption(item);
return true;
@@ -288,7 +288,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public void onCloseNote() {
if (!titleModified && originalNote == null && getContent().isEmpty()) {
- db.deleteNoteAndSync(localAccount, note.getId());
+ repo.deleteNoteAndSync(localAccount, note.getId());
}
}
@@ -304,13 +304,13 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
if (note.getContent().equals(newContent)) {
if (note.getScrollY() != originalScrollY) {
Log.v(TAG, "... only saving new scroll state, since content did not change");
- db.getNoteDao().updateScrollY(note.getId(), note.getScrollY());
+ repo.updateScrollY(note.getId(), note.getScrollY());
} else {
Log.v(TAG, "... not saving, since nothing has changed");
}
} else {
// FIXME requires database queries on main thread!
- note = db.updateNoteAndSync(localAccount, note, newContent, null, callback);
+ note = repo.updateNoteAndSync(localAccount, note, newContent, null, callback);
listener.onNoteUpdated(note);
requireActivity().invalidateOptionsMenu();
}
@@ -354,7 +354,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
@Override
public void onCategoryChosen(String category) {
- db.setCategory(localAccount, note.getId(), category);
+ repo.setCategory(localAccount, note.getId(), category);
note.setCategory(category);
listener.onNoteUpdated(note);
}
@@ -364,13 +364,13 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
titleModified = true;
note.setTitle(newTitle);
new Thread(() -> {
- note = db.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null);
+ note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null);
requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note));
}).start();
}
public void moveNote(Account account) {
- db.moveNoteToAnotherAccount(account, note);
+ repo.moveNoteToAnotherAccount(account, note);
listener.close();
}
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 fca142e9..f1626149 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
@@ -125,7 +125,7 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
protected void registerInternalNoteLinkHandler() {
binding.singleNoteContent.registerOnLinkClickCallback((link) -> {
try {
- final long noteLocalId = db.getNoteDao().getLocalIdByRemoteId(this.note.getAccountId(), Long.parseLong(link));
+ final long noteLocalId = repo.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;
@@ -153,20 +153,20 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
@Override
public void onRefresh() {
- if (noteLoaded && db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) {
+ if (noteLoaded && repo.isSyncPossible() && SSOUtil.isConfigured(getContext())) {
binding.swiperefreshlayout.setRefreshing(true);
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());
+ final Account account = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()).name);
+ repo.addCallbackPull(account, () -> new Thread(() -> {
+ note = repo.getNoteById(note.getId());
changedText = note.getContent();
requireActivity().runOnUiThread(() -> {
binding.singleNoteContent.setMarkdownString(note.getContent());
binding.swiperefreshlayout.setRefreshing(false);
});
}).start());
- db.getNoteServerSyncHelper().scheduleSync(account, false);
+ repo.scheduleSync(account, false);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
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
index 96900a3b..ea5efd37 100644
--- 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
@@ -11,7 +11,7 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
import static androidx.lifecycle.Transformations.map;
import static androidx.lifecycle.Transformations.switchMap;
@@ -19,14 +19,14 @@ import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCat
public class CategoryViewModel extends AndroidViewModel {
- private final NotesDatabase db;
+ private final NotesRepository repo;
@NonNull
private final MutableLiveData<String> searchTerm = new MutableLiveData<>("");
public CategoryViewModel(@NonNull Application application) {
super(application);
- db = NotesDatabase.getInstance(application);
+ repo = NotesRepository.getInstance(application);
}
public void postSearchTerm(@NonNull String searchTerm) {
@@ -36,7 +36,7 @@ public class CategoryViewModel extends AndroidViewModel {
@NonNull
public LiveData<List<NavigationItem.CategoryNavigationItem>> getCategories(long accountId) {
return switchMap(this.searchTerm, searchTerm ->
- map(db.getNoteDao().searchCategories$(accountId, TextUtils.isEmpty(searchTerm) ? "%" : "%" + searchTerm + "%"),
+ map(repo.searchCategories$(accountId, TextUtils.isEmpty(searchTerm) ? "%" : "%" + searchTerm + "%"),
categories -> convertToCategoryNavigationItem(getApplication(), categories)));
}
}
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
index 2695f08a..c942b345 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
@@ -1,34 +1,36 @@
package it.niedermann.owncloud.notes.importaccount;
+import android.accounts.NetworkErrorException;
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.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.UnknownErrorException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.ui.UiExceptionManager;
+import java.net.HttpURLConnection;
+
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.ApiProvider;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
public class ImportAccountActivity extends AppCompatActivity {
@@ -52,6 +54,7 @@ public class ImportAccountActivity extends AppCompatActivity {
binding.welcomeText.setText(getString(R.string.welcome_text, getString(R.string.app_name)));
binding.addButton.setOnClickListener((v) -> {
binding.addButton.setEnabled(false);
+ binding.status.setVisibility(View.GONE);
try {
AccountImporter.pickNewAccount(this);
} catch (NextcloudFilesAppNotInstalledException e) {
@@ -86,29 +89,50 @@ public class ImportAccountActivity extends AppCompatActivity {
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());
+ importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account account) {
+ runOnUiThread(() -> {
+ Log.i(TAG, capabilities.toString());
+ BrandingUtil.saveBrandColors(ImportAccountActivity.this, capabilities.getColor(), capabilities.getTextColor());
+ setResult(RESULT_OK);
+ finish();
+ });
}
- }));
- } catch (Throwable e) {
- e.printStackTrace();
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ binding.addButton.setEnabled(true);
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
+ }
+ });
+ } catch (Throwable t) {
+ t.printStackTrace();
+ ApiProvider.invalidateAPICache(ssoAccount);
+ SingleAccountHelper.setCurrentAccount(this, null);
runOnUiThread(() -> {
- binding.addButton.setEnabled(true);
- ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ restoreCleanState();
+ if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ binding.status.setText(R.string.error_maintenance_mode);
+ binding.status.setVisibility(View.VISIBLE);
+ } else if (t instanceof NetworkErrorException) {
+ binding.status.setText(getString(R.string.error_sync, getString(R.string.error_no_network)));
+ binding.status.setVisibility(View.VISIBLE);
+ } else if (t instanceof UnknownErrorException && t.getMessage().contains("No address associated with hostname")) {
+ // https://github.com/stefan-niedermann/nextcloud-notes/issues/1014
+ binding.status.setText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account);
+ binding.status.setVisibility(View.VISIBLE);
+ } else {
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
});
}
}).start();
});
} catch (AccountImportCancelledException e) {
- runOnUiThread(() -> binding.addButton.setEnabled(true));
+ restoreCleanState();
Log.i(TAG, "Account import has been canceled.");
}
}
@@ -118,4 +142,11 @@ public class ImportAccountActivity extends AppCompatActivity {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
+
+ private void restoreCleanState() {
+ runOnUiThread(() -> {
+ binding.addButton.setEnabled(true);
+ binding.progressCircular.setVisibility(View.GONE);
+ });
+ }
} \ 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
index 04b30d8d..905a59b1 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
@@ -6,23 +6,24 @@ 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.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
public class ImportAccountViewModel extends AndroidViewModel {
private static final String TAG = ImportAccountViewModel.class.getSimpleName();
@NonNull
- private final NotesDatabase db;
+ private final NotesRepository repo;
public ImportAccountViewModel(@NonNull Application application) {
super(application);
- this.db = NotesDatabase.getInstance(application.getApplicationContext());
+ this.repo = NotesRepository.getInstance(application);
}
- public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
- return db.addAccount(url, username, accountName, capabilities);
+ public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
+ repo.addAccount(url, username, accountName, capabilities, callback);
}
}
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 3600b0d6..d889689d 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,5 +1,6 @@
package it.niedermann.owncloud.notes.main;
+import android.accounts.NetworkErrorException;
import android.animation.AnimatorInflater;
import android.app.SearchManager;
import android.content.Intent;
@@ -39,10 +40,13 @@ 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.exceptions.UnknownErrorException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.LinkedList;
@@ -71,6 +75,7 @@ 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.ApiProvider;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@@ -267,19 +272,24 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
.apply(RequestOptions.circleCropTransform())
.into(activityBinding.launchAccountSwitcher);
- mainViewModel.synchronizeNotes(nextAccount, new IResponseCallback() {
+ mainViewModel.synchronizeNotes(nextAccount, new IResponseCallback<Void>() {
@Override
- public void onSuccess() {
+ public void onSuccess(Void v) {
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) {
+ if (t instanceof IntendedOfflineException) {
Log.i(TAG, "Capabilities and notes not updated because " + nextAccount.getAccountName() + " is offline by intention.");
- } else {
+ } else if (t instanceof NetworkErrorException) {
BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
}
});
}
@@ -309,17 +319,26 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
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());
- }
+ try {
+ // It is possible that after the deletion of the last account, this onResponse gets called before the ImportAccountActivity gets started.
+ if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) != null) {
+ mainViewModel.synchronizeNotes(currentAccount, new IResponseCallback<Void>() {
+ @Override
+ public void onSuccess(Void v) {
+ Log.d(TAG, "Successfully synchronized notes for " + currentAccount.getAccountName());
+ }
- @Override
- public void onError(@NonNull Throwable t) {
- t.printStackTrace();
+ @Override
+ public void onError(@NonNull Throwable t) {
+ t.printStackTrace();
+ }
+ });
}
- });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } catch (NoCurrentAccountSelectedException e) {
+ Log.i(TAG, "No current account is selected - maybe the last account has been deleted?");
+ }
});
super.onResume();
}
@@ -428,9 +447,9 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
final LiveData<Account> syncLiveData = mainViewModel.getCurrentAccount();
final Observer<Account> syncObserver = currentAccount -> {
syncLiveData.removeObservers(this);
- mainViewModel.synchronizeCapabilitiesAndNotes(currentAccount, new IResponseCallback() {
+ mainViewModel.synchronizeCapabilitiesAndNotes(currentAccount, new IResponseCallback<Void>() {
@Override
- public void onSuccess() {
+ public void onSuccess(Void v) {
Log.d(TAG, "Successfully synchronized capabilities and notes for " + currentAccount.getAccountName());
}
@@ -438,10 +457,17 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
public void onError(@NonNull Throwable t) {
runOnUiThread(() -> {
swipeRefreshLayout.setRefreshing(false);
- if (t.getClass() == IntendedOfflineException.class || t instanceof IntendedOfflineException) {
+ if (t instanceof IntendedOfflineException) {
Log.i(TAG, "Capabilities and notes not updated because " + currentAccount.getAccountName() + " is offline by intention.");
- } else {
+ } else if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_maintenance_mode, Snackbar.LENGTH_LONG).show();
+ } else if (t instanceof NetworkErrorException) {
BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
}
});
}
@@ -631,15 +657,23 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
try {
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null);
- 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) {
+ mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account result) {
+ new Thread(() -> {
+ Log.i(TAG, capabilities.toString());
+ final Account a = mainViewModel.getLocalAccountByAccountName(ssoAccount.name);
+ runOnUiThread(() -> mainViewModel.postCurrentAccount(a));
+ }).start();
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+ });
+ } catch (Throwable e) {
+ ApiProvider.invalidateAPICache(ssoAccount);
// Happens when importing an already existing account the second time
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.");
@@ -648,6 +682,9 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
// 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());
});
+ } else if (e instanceof UnknownErrorException && e.getMessage().contains("No address associated with hostname")) {
+ // https://github.com/stefan-niedermann/nextcloud-notes/issues/1014
+ runOnUiThread(() -> Snackbar.make(coordinatorLayout, R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account, Snackbar.LENGTH_LONG).show());
} else {
e.printStackTrace();
runOnUiThread(() -> {
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
index 0241951e..ec0e71c7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -21,7 +21,6 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundExce
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;
@@ -34,8 +33,7 @@ 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.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
import it.niedermann.owncloud.notes.persistence.entity.Note;
@@ -60,6 +58,7 @@ import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.
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;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
public class MainViewModel extends AndroidViewModel {
@@ -73,7 +72,7 @@ public class MainViewModel extends AndroidViewModel {
private static final String KEY_EXPANDED_CATEGORY = "expandedCategory";
@NonNull
- private final NotesDatabase db;
+ private final NotesRepository repo;
@NonNull
private final MutableLiveData<Account> currentAccount = new MutableLiveData<>();
@@ -86,7 +85,7 @@ public class MainViewModel extends AndroidViewModel {
public MainViewModel(@NonNull Application application, @NonNull SavedStateHandle savedStateHandle) {
super(application);
- this.db = NotesDatabase.getInstance(application);
+ this.repo = NotesRepository.getInstance(application);
this.state = savedStateHandle;
}
@@ -175,7 +174,7 @@ public class MainViewModel extends AndroidViewModel {
@NonNull
@MainThread
public LiveData<Pair<NavigationCategory, CategorySortingMethod>> getCategorySortingMethodOfSelectedCategory() {
- return switchMap(getSelectedCategory(), selectedCategory -> map(db.getCategoryOrder(selectedCategory), sortingMethod -> new Pair<>(selectedCategory, sortingMethod)));
+ return switchMap(getSelectedCategory(), selectedCategory -> map(repo.getCategoryOrder(selectedCategory), sortingMethod -> new Pair<>(selectedCategory, sortingMethod)));
}
public LiveData<Void> modifyCategoryOrder(@NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
@@ -184,7 +183,7 @@ public class MainViewModel extends AndroidViewModel {
return new MutableLiveData<>(null);
} else {
Log.v(TAG, "[modifyCategoryOrder] - currentAccount: " + currentAccount.getAccountName());
- db.modifyCategoryOrder(currentAccount.getId(), selectedCategory, sortingMethod);
+ repo.modifyCategoryOrder(currentAccount.getId(), selectedCategory, sortingMethod);
return new MutableLiveData<>(null);
}
});
@@ -225,22 +224,22 @@ public class MainViewModel extends AndroidViewModel {
case RECENT: {
Log.v(TAG, "[getNotesListLiveData] - category: " + RECENT);
fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
- ? db.getNoteDao().searchRecentByModified$(accountId, searchQueryOrWildcard)
- : db.getNoteDao().searchRecentLexicographically$(accountId, searchQueryOrWildcard);
+ ? repo.searchRecentByModified$(accountId, searchQueryOrWildcard)
+ : repo.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);
+ ? repo.searchFavoritesByModified$(accountId, searchQueryOrWildcard)
+ : repo.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);
+ ? repo.searchUncategorizedByModified$(accountId, searchQueryOrWildcard)
+ : repo.searchUncategorizedLexicographically$(accountId, searchQueryOrWildcard);
break;
}
case DEFAULT_CATEGORY:
@@ -251,8 +250,8 @@ public class MainViewModel extends AndroidViewModel {
}
Log.v(TAG, "[getNotesListLiveData] - category: " + category);
fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
- ? db.getNoteDao().searchCategoryByModified$(accountId, searchQueryOrWildcard, category)
- : db.getNoteDao().searchCategoryLexicographically$(accountId, searchQueryOrWildcard, category);
+ ? repo.searchCategoryByModified$(accountId, searchQueryOrWildcard, category)
+ : repo.searchCategoryLexicographically$(accountId, searchQueryOrWildcard, category);
break;
}
}
@@ -294,11 +293,11 @@ public class MainViewModel extends AndroidViewModel {
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) -> {
+ return switchMap(repo.count$(currentAccount.getId()), (count) -> {
Log.v(TAG, "[getNavigationCategories] - count: " + count);
- return switchMap(db.getNoteDao().countFavorites$(currentAccount.getId()), (favoritesCount) -> {
+ return switchMap(repo.countFavorites$(currentAccount.getId()), (favoritesCount) -> {
Log.v(TAG, "[getNavigationCategories] - favoritesCount: " + favoritesCount);
- return distinctUntilChanged(map(db.getNoteDao().getCategories$(currentAccount.getId()), fromDatabase ->
+ return distinctUntilChanged(map(repo.getCategories$(currentAccount.getId()), fromDatabase ->
fromCategoriesWithNotesCount(getApplication(), expandedCategory, fromDatabase, count, favoritesCount)
));
});
@@ -369,11 +368,11 @@ public class MainViewModel extends AndroidViewModel {
return items;
}
- public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
+ public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback<Void> callback) {
Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize capabilities for " + localAccount.getAccountName());
- synchronizeCapabilities(localAccount, new IResponseCallback() {
+ synchronizeCapabilities(localAccount, new IResponseCallback<Void>() {
@Override
- public void onSuccess() {
+ public void onSuccess(Void v) {
Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize notes for " + localAccount.getAccountName());
synchronizeNotes(localAccount, callback);
}
@@ -388,35 +387,36 @@ public class MainViewModel extends AndroidViewModel {
/**
* 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) {
+ public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback<Void> callback) {
new Thread(() -> {
- final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
- if (!syncHelper.isSyncPossible()) {
- syncHelper.updateNetworkStatus();
+ if (!repo.isSyncPossible()) {
+ repo.updateNetworkStatus();
}
- if (syncHelper.isSyncPossible()) {
+ if (repo.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());
+ repo.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
+ repo.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();
+ repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
+ callback.onSuccess(null);
} catch (NextcloudFilesAppAccountNotFoundException e) {
- db.getAccountDao().deleteAccount(localAccount);
+ repo.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);
+ } catch (Throwable t) {
+ if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) {
+ if (((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_NOT_MODIFIED) {
+ Log.d(TAG, "Server returned HTTP Status Code " + ((NextcloudHttpRequestFailedException) t).getStatusCode() + " - Capabilities not modified.");
+ callback.onSuccess(null);
+ return;
+ }
}
+ callback.onError(t);
}
} else {
- if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
+ if (repo.isNetworkConnected() && repo.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."));
@@ -428,18 +428,17 @@ public class MainViewModel extends AndroidViewModel {
/**
* 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) {
+ public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback<Void> callback) {
new Thread(() -> {
Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
- final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
- if (!syncHelper.isSyncPossible()) {
- syncHelper.updateNetworkStatus();
+ if (!repo.isSyncPossible()) {
+ repo.updateNetworkStatus();
}
- if (syncHelper.isSyncPossible()) {
- syncHelper.scheduleSync(currentAccount, false);
- callback.onSuccess();
+ if (repo.isSyncPossible()) {
+ repo.scheduleSync(currentAccount, false);
+ callback.onSuccess(null);
} else { // Sync is not possible
- if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
+ if (repo.isNetworkConnected() && repo.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."));
@@ -449,25 +448,25 @@ public class MainViewModel extends AndroidViewModel {
}
public LiveData<Boolean> getSyncStatus() {
- return db.getNoteServerSyncHelper().getSyncStatus();
+ return repo.getSyncStatus();
}
public LiveData<ArrayList<Throwable>> getSyncErrors() {
- return db.getNoteServerSyncHelper().getSyncErrors();
+ return repo.getSyncErrors();
}
public LiveData<Boolean> hasMultipleAccountsConfigured() {
- return map(db.getAccountDao().countAccounts$(), (counter) -> counter != null && counter > 1);
+ return map(repo.countAccounts$(), (counter) -> counter != null && counter > 1);
}
@WorkerThread
public Account getLocalAccountByAccountName(String accountName) {
- return db.getAccountDao().getAccountByName(accountName);
+ return repo.getAccountByName(accountName);
}
@WorkerThread
public List<Account> getAccounts() {
- return db.getAccountDao().getAccounts();
+ return repo.getAccounts();
}
public LiveData<Void> setCategory(Iterable<Long> noteIds, @NonNull String category) {
@@ -477,7 +476,7 @@ public class MainViewModel extends AndroidViewModel {
} else {
Log.v(TAG, "[setCategory] - currentAccount: " + currentAccount.getAccountName());
for (Long noteId : noteIds) {
- db.setCategory(currentAccount, noteId, category);
+ repo.setCategory(currentAccount, noteId, category);
}
return new MutableLiveData<>(null);
}
@@ -485,9 +484,9 @@ public class MainViewModel extends AndroidViewModel {
}
public LiveData<Note> moveNoteToAnotherAccount(Account account, Long noteId) {
- return switchMap(db.getNoteDao().getNoteById$(noteId), (note) -> {
+ return switchMap(repo.getNoteById$(noteId), (note) -> {
Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + note);
- return db.moveNoteToAnotherAccount(account, note);
+ return repo.moveNoteToAnotherAccount(account, note);
});
}
@@ -497,7 +496,7 @@ public class MainViewModel extends AndroidViewModel {
return new MutableLiveData<>(null);
} else {
Log.v(TAG, "[toggleFavoriteAndSync] - currentAccount: " + currentAccount.getAccountName());
- db.toggleFavoriteAndSync(currentAccount, noteId);
+ repo.toggleFavoriteAndSync(currentAccount, noteId);
return new MutableLiveData<>(null);
}
});
@@ -509,7 +508,7 @@ public class MainViewModel extends AndroidViewModel {
return new MutableLiveData<>(null);
} else {
Log.v(TAG, "[deleteNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
- db.deleteNoteAndSync(currentAccount, id);
+ repo.deleteNoteAndSync(currentAccount, id);
return new MutableLiveData<>(null);
}
});
@@ -522,15 +521,15 @@ public class MainViewModel extends AndroidViewModel {
} else {
Log.v(TAG, "[deleteNotesAndSync] - currentAccount: " + currentAccount.getAccountName());
for (Long id : ids) {
- db.deleteNoteAndSync(currentAccount, id);
+ repo.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 void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
+ repo.addAccount(url, username, accountName, capabilities, callback);
}
public LiveData<Note> getFullNote$(long id) {
@@ -539,7 +538,7 @@ public class MainViewModel extends AndroidViewModel {
@WorkerThread
public Note getFullNote(long id) {
- return db.getNoteDao().getNoteById(id);
+ return repo.getNoteById(id);
}
public LiveData<List<Note>> getFullNotesWithCategory(@NonNull Collection<Long> ids) {
@@ -552,7 +551,7 @@ public class MainViewModel extends AndroidViewModel {
new Thread(() -> notes.postValue(
ids
.stream()
- .map(id -> db.getNoteDao().getNoteById(id))
+ .map(repo::getNoteById)
.collect(Collectors.toList())
)).start();
return notes;
@@ -566,7 +565,7 @@ public class MainViewModel extends AndroidViewModel {
return new MutableLiveData<>();
} else {
Log.v(TAG, "[addNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
- return db.addNoteAndSync(currentAccount, note);
+ return repo.addNoteAndSync(currentAccount, note);
}
});
}
@@ -575,25 +574,25 @@ public class MainViewModel extends AndroidViewModel {
return switchMap(getCurrentAccount(), currentAccount -> {
if (currentAccount != null) {
Log.v(TAG, "[updateNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
- db.updateNoteAndSync(currentAccount, oldNote, newContent, newTitle, null);
+ repo.updateNoteAndSync(currentAccount, oldNote, newContent, newTitle, null);
}
return new MutableLiveData<>(null);
});
}
public void createOrUpdateSingleNoteWidgetData(SingleNoteWidgetData data) {
- db.getWidgetSingleNoteDao().createOrUpdateSingleNoteWidgetData(data);
+ repo.createOrUpdateSingleNoteWidgetData(data);
}
public LiveData<Integer> getAccountsCount() {
- return db.getAccountDao().countAccounts$();
+ return repo.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 Note fullNote = repo.getNoteById(noteId);
final String tempFullNote = fullNote.getContent();
if (!TextUtils.isEmpty(tempFullNote)) {
if (noteContents.length() > 0) {
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 c1a4e139..25155c9a 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
@@ -57,16 +57,7 @@ public class ManageAccountAdapter extends RecyclerView.Adapter<ManageAccountView
holder.bind(localAccount, (localAccountClicked) -> {
setCurrentLocalAccount(localAccountClicked);
onAccountClick.accept(localAccountClicked);
- }, (localAccountToDelete -> {
- for (int i = 0; i < localAccounts.size(); i++) {
- if (localAccounts.get(i).getId() == localAccountToDelete.getId()) {
- localAccounts.remove(i);
- notifyItemRemoved(i);
- break;
- }
- }
- onAccountDelete.accept(localAccountToDelete);
- }), onChangeNotesPath, onChangeFileSuffix, currentLocalAccount != null && currentLocalAccount.getId() == localAccount.getId());
+ }, onAccountDelete, onChangeNotesPath, onChangeFileSuffix, currentLocalAccount != null && currentLocalAccount.getId() == localAccount.getId());
}
@Override
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 408c1f2d..01eafd62 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
@@ -1,5 +1,6 @@
package it.niedermann.owncloud.notes.manageaccounts;
+import android.accounts.NetworkErrorException;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
@@ -14,98 +15,104 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AlertDialog;
-import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
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.List;
import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder;
+import it.niedermann.owncloud.notes.branding.BrandedDeleteAlertDialogBuilder;
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.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
-import it.niedermann.owncloud.notes.shared.model.ServerSettings;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.NotesSettings;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
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 ManageAccountsViewModel viewModel;
private ManageAccountAdapter adapter;
- private NotesDatabase db = null;
- private final List<Account> localAccounts = new ArrayList<>();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityManageAccountsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this).get(ManageAccountsViewModel.class);
+ setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- db = NotesDatabase.getInstance(this);
-
- 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();
+ adapter = new ManageAccountAdapter(
+ this::selectAccount,
+ this::deleteAccount,
+ this::onChangeNotesPath,
+ this::onChangeFileSuffix
+ );
+ binding.accounts.setAdapter(adapter);
+
+ viewModel.getAccounts$().observe(this, (accounts) -> {
+ if (accounts == null || accounts.size() < 1) {
+ finish();
+ return;
}
- binding.accounts.setAdapter(adapter);
+ this.adapter.setLocalAccounts(accounts);
+ viewModel.getCurrentAccount(this, new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account result) {
+ runOnUiThread(() -> adapter.setCurrentLocalAccount(result));
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> adapter.setCurrentLocalAccount(null));
+ t.printStackTrace();
+ }
+ });
});
}
- 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;
- }
+ private void selectAccount(@NonNull Account accountToSelect) {
+ viewModel.selectAccount(accountToSelect, this);
+ }
+
+ private void deleteAccount(@NonNull Account accountToDelete) {
+ viewModel.countUnsynchronizedNotes(accountToDelete.getId(), new IResponseCallback<Long>() {
+ @Override
+ public void onSuccess(Long unsynchronizedChangesCount) {
+ runOnUiThread(() -> {
+ if (unsynchronizedChangesCount > 0) {
+ new BrandedDeleteAlertDialogBuilder(ManageAccountsActivity.this)
+ .setTitle(getString(R.string.remove_account, accountToDelete.getUserName()))
+ .setMessage(getResources().getQuantityString(R.plurals.remove_account_message, (int) unsynchronizedChangesCount.longValue(), accountToDelete.getAccountName(), unsynchronizedChangesCount))
+ .setNeutralButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.simple_remove, (d, l) -> viewModel.deleteAccount(accountToDelete, ManageAccountsActivity.this))
+ .show();
+ } else {
+ viewModel.deleteAccount(accountToDelete, ManageAccountsActivity.this);
+ }
+ });
}
- if (localAccounts.size() > 0) {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), localAccounts.get(0).getAccountName());
- adapter.setCurrentLocalAccount(localAccounts.get(0));
- } else {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), null);
- finish();
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
- deleteLiveData.removeObservers(this);
});
}
private void onChangeNotesPath(@NonNull Account localAccount) {
- final NotesClient client = NotesClient.newInstance(localAccount.getPreferredApiVersion(), getApplicationContext());
+ final NotesRepository repository = NotesRepository.getInstance(getApplicationContext());
final EditText editText = new EditText(this);
editText.setEnabled(false);
final View wrapper = createDialogViewWrapper(editText);
@@ -116,27 +123,55 @@ public class ManageAccountsActivity extends LockedActivity {
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_edit_save, (v, d) -> new Thread(() -> {
try {
- final ServerSettings newSettings = client.putServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), new ServerSettings(editText.getText().toString(), null));
- Toast.makeText(this, "New notes path: " + newSettings.getNotesPath(), Toast.LENGTH_LONG).show();
- } catch (Exception e) {
+ final Call<NotesSettings> putSettingsCall = repository.putServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), new NotesSettings(editText.getText().toString(), null), localAccount.getPreferredApiVersion());
+ putSettingsCall.enqueue(new Callback<NotesSettings>() {
+ @Override
+ public void onResponse(@NonNull Call<NotesSettings> call, @NonNull Response<NotesSettings> response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(ManageAccountsActivity.this, "New notes path: " + response.body().getNotesPath(), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(ManageAccountsActivity.this, "HTTP status code: " + response.code(), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call<NotesSettings> call, @NonNull Throwable t) {
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+ });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
}).start())
.show();
- new Thread(() -> {
- try {
- final ServerSettings oldSettings = client.getServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()));
- editText.setText(oldSettings.getNotesPath());
- editText.setEnabled(true);
- } catch (Exception e) {
- dialog.dismiss();
- ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- }).start();
+ try {
+ final Call<NotesSettings> oldSettingsCall = repository.getServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), localAccount.getPreferredApiVersion());
+ oldSettingsCall.enqueue(new Callback<NotesSettings>() {
+ @Override
+ public void onResponse(@NonNull Call<NotesSettings> call, @NonNull Response<NotesSettings> response) {
+ runOnUiThread(() -> {
+ if (response.isSuccessful()) {
+ editText.setText(response.body().getNotesPath());
+ editText.setEnabled(true);
+ } else {
+ ExceptionDialogFragment.newInstance(new NetworkErrorException("HTTP status code: " + response.code())).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(@NonNull Call<NotesSettings> call, @NonNull Throwable t) {
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ dialog.dismiss();
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
}
private void onChangeFileSuffix(@NonNull Account localAccount) {
- final NotesClient client = NotesClient.newInstance(localAccount.getPreferredApiVersion(), getApplicationContext());
+ final NotesRepository repository = NotesRepository.getInstance(getApplicationContext());
final Spinner spinner = new Spinner(this);
spinner.setEnabled(false);
final View wrapper = createDialogViewWrapper(spinner);
@@ -150,28 +185,57 @@ public class ManageAccountsActivity extends LockedActivity {
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton("Save", (v, d) -> new Thread(() -> {
try {
- final ServerSettings newSettings = client.putServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), new ServerSettings(null, spinner.getSelectedItem().toString()));
- Toast.makeText(this, "New file suffix: " + newSettings.getNotesPath(), Toast.LENGTH_LONG).show();
- } catch (Exception e) {
+ final Call<NotesSettings> putSettingsCall = repository.putServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), new NotesSettings(null, spinner.getSelectedItem().toString()), localAccount.getPreferredApiVersion());
+ putSettingsCall.enqueue(new Callback<NotesSettings>() {
+ @Override
+ public void onResponse(@NonNull Call<NotesSettings> call, @NonNull Response<NotesSettings> response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(ManageAccountsActivity.this, "New file suffix: " + response.body().getNotesPath(), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(ManageAccountsActivity.this, "HTTP status code: " + response.code(), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call<NotesSettings> call, @NonNull Throwable t) {
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+ });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
}).start())
.show();
- new Thread(() -> {
- try {
- final ServerSettings oldSettings = client.getServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()));
- for (int i = 0; i < adapter.getCount(); i++) {
- if (adapter.getItem(i).equals(oldSettings.getFileSuffix())) {
- spinner.setSelection(i);
- break;
- }
+ try {
+ final Call<NotesSettings> oldSettingsCall = repository.getServerSettings(AccountImporter.getSingleSignOnAccount(this, localAccount.getAccountName()), localAccount.getPreferredApiVersion());
+ oldSettingsCall.enqueue(new Callback<NotesSettings>() {
+ @Override
+ public void onResponse(@NonNull Call<NotesSettings> call, @NonNull Response<NotesSettings> response) {
+ runOnUiThread(() -> {
+ if (response.isSuccessful()) {
+ for (int i = 0; i < adapter.getCount(); i++) {
+ if (adapter.getItem(i).equals(response.body().getFileSuffix())) {
+ spinner.setSelection(i);
+ break;
+ }
+ }
+ spinner.setEnabled(true);
+ } else {
+ ExceptionDialogFragment.newInstance(new Exception("HTTP status code: " + response.code())).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
- spinner.setEnabled(true);
- } catch (Exception e) {
- dialog.dismiss();
- ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- }).start();
+
+ @Override
+ public void onFailure(@NonNull Call<NotesSettings> call, @NonNull Throwable t) {
+ dialog.dismiss();
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ dialog.dismiss();
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
}
@NonNull
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java
new file mode 100644
index 00000000..2ee45cf8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java
@@ -0,0 +1,74 @@
+package it.niedermann.owncloud.notes.manageaccounts;
+
+import android.app.Application;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+public class ManageAccountsViewModel extends AndroidViewModel {
+
+ private static final String TAG = ManageAccountsViewModel.class.getSimpleName();
+
+ @NonNull
+ private final NotesRepository repo;
+
+ public ManageAccountsViewModel(@NonNull Application application) {
+ super(application);
+ this.repo = NotesRepository.getInstance(application);
+ }
+
+ public void getCurrentAccount(@NonNull Context context, @NonNull IResponseCallback<Account> callback) {
+ try {
+ callback.onSuccess(repo.getAccountByName((SingleAccountHelper.getCurrentSingleSignOnAccount(context).name)));
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ callback.onError(e);
+ }
+ }
+
+ public LiveData<List<Account>> getAccounts$() {
+ return distinctUntilChanged(repo.getAccounts$());
+ }
+
+ public void deleteAccount(@NonNull Account account, @NonNull Context context) {
+ new Thread(() -> {
+ final List<Account> accounts = repo.getAccounts();
+ for (int i = 0; i < accounts.size(); i++) {
+ if (accounts.get(i).getId() == account.getId()) {
+ if (i > 0) {
+ selectAccount(accounts.get(i - 1), context);
+ } else if (accounts.size() > 1) {
+ selectAccount(accounts.get(i + 1), context);
+ } else {
+ selectAccount(null, context);
+ }
+ repo.deleteAccount(accounts.get(i));
+ break;
+ }
+ }
+ }).start();
+ }
+
+ public void selectAccount(@Nullable Account account, @NonNull Context context) {
+ SingleAccountHelper.setCurrentAccount(context, (account == null) ? null : account.getAccountName());
+ }
+
+ public void countUnsynchronizedNotes(long accountId, @NonNull IResponseCallback<Long> callback) {
+ new Thread(() -> callback.onSuccess(repo.countUnsynchronizedNotes(accountId))).start();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
new file mode 100644
index 00000000..b1bca215
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
@@ -0,0 +1,137 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializer;
+import com.nextcloud.android.sso.api.NextcloudAPI;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer;
+import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
+import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import retrofit2.NextcloudRetrofitApiBuilder;
+import retrofit2.Retrofit;
+
+/**
+ * Since creating APIs via {@link Retrofit} uses reflection and {@link NextcloudAPI} <a href="https://github.com/nextcloud/Android-SingleSignOn/issues/120#issuecomment-540069990">is supposed to stay alive as long as possible</a>, those artifacts are going to be cached.
+ * They can be invalidated by using either {@link #invalidateAPICache()} for all or {@link #invalidateAPICache(SingleSignOnAccount)} for a specific {@link SingleSignOnAccount} and will be recreated when they are queried the next time.
+ */
+@WorkerThread
+public class ApiProvider {
+
+ private static final String TAG = ApiProvider.class.getSimpleName();
+
+ private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
+
+ private static final Map<String, NextcloudAPI> API_CACHE = new HashMap<>();
+
+ private static final Map<String, OcsAPI> API_CACHE_OCS = new HashMap<>();
+ private static final Map<String, NotesAPI> API_CACHE_NOTES = new HashMap<>();
+
+ /**
+ * An {@link OcsAPI} currently shares the {@link Gson} configuration with the {@link NotesAPI} and therefore divides all {@link Calendar} milliseconds by 1000 while serializing and multiplies values by 1000 during deserialization.
+ */
+ public static synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ if (API_CACHE_OCS.containsKey(ssoAccount.name)) {
+ return API_CACHE_OCS.get(ssoAccount.name);
+ }
+ final OcsAPI ocsAPI = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_OCS).create(OcsAPI.class);
+ API_CACHE_OCS.put(ssoAccount.name, ocsAPI);
+ return ocsAPI;
+ }
+
+ /**
+ * In case the {@param preferredApiVersion} changes, call {@link #invalidateAPICache(SingleSignOnAccount)} or {@link #invalidateAPICache()} to make sure that this call returns a {@link NotesAPI} that uses the correct compatibility layer.
+ */
+ public static synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) {
+ if (API_CACHE_NOTES.containsKey(ssoAccount.name)) {
+ return API_CACHE_NOTES.get(ssoAccount.name);
+ }
+ final NotesAPI notesAPI = new NotesAPI(getNextcloudAPI(context, ssoAccount), preferredApiVersion);
+ API_CACHE_NOTES.put(ssoAccount.name, notesAPI);
+ return notesAPI;
+ }
+
+ private static synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ if (API_CACHE.containsKey(ssoAccount.name)) {
+ return API_CACHE.get(ssoAccount.name);
+ } else {
+ Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
+ final NextcloudAPI nextcloudAPI = new NextcloudAPI(context.getApplicationContext(), ssoAccount,
+ new GsonBuilder()
+ .excludeFieldsWithoutExposeAnnotation()
+ .registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer<Calendar>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.getTimeInMillis() / 1_000))
+ .registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer<Calendar>) (src, typeOfSrc, ctx) -> {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(src.getAsLong() * 1_000);
+ return calendar;
+ })
+ .registerTypeAdapter(Capabilities.class, new CapabilitiesDeserializer())
+ .create(), new NextcloudAPI.ApiConnectedListener() {
+ @Override
+ public void onConnected() {
+ Log.i(TAG, "SSO API connected for " + ssoAccount);
+ }
+
+ @Override
+ public void onError(Exception ex) {
+ ex.printStackTrace();
+ invalidateAPICache(ssoAccount);
+ }
+ });
+ API_CACHE.put(ssoAccount.name, nextcloudAPI);
+ return nextcloudAPI;
+ }
+ }
+
+ /**
+ * Invalidates the API cache for the given {@param ssoAccount}
+ *
+ * @param ssoAccount the ssoAccount for which the API cache should be cleared.
+ */
+ public static synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
+ Log.v(TAG, "Invalidating API cache for " + ssoAccount.name);
+ if (API_CACHE.containsKey(ssoAccount.name)) {
+ final NextcloudAPI nextcloudAPI = API_CACHE.get(ssoAccount.name);
+ if (nextcloudAPI != null) {
+ nextcloudAPI.stop();
+ }
+ API_CACHE.remove(ssoAccount.name);
+ }
+ API_CACHE_NOTES.remove(ssoAccount.name);
+ API_CACHE_OCS.remove(ssoAccount.name);
+ }
+
+ /**
+ * Invalidates the whole API cache for all accounts
+ */
+ public static synchronized void invalidateAPICache() {
+ for (String key : API_CACHE.keySet()) {
+ Log.v(TAG, "Invalidating API cache for " + key);
+ if (API_CACHE.containsKey(key)) {
+ final NextcloudAPI nextcloudAPI = API_CACHE.get(key);
+ if (nextcloudAPI != null) {
+ nextcloudAPI.stop();
+ }
+ API_CACHE.remove(key);
+ }
+ }
+ API_CACHE_NOTES.clear();
+ API_CACHE_OCS.clear();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java
index 9a08bc1e..8afc64b8 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java
@@ -1,26 +1,18 @@
package it.niedermann.owncloud.notes.persistence;
import android.content.Context;
-import android.content.pm.PackageInfo;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
-import com.nextcloud.android.sso.aidl.NextcloudRequest;
-import com.nextcloud.android.sso.api.AidlNetworkRequest;
-import com.nextcloud.android.sso.api.Response;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException;
+import com.nextcloud.android.sso.api.ParsedResponse;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@WorkerThread
@@ -28,60 +20,24 @@ public class CapabilitiesClient {
private static final String TAG = CapabilitiesClient.class.getSimpleName();
- private static final int MIN_NEXTCLOUD_FILES_APP_VERSION_CODE = 30090000;
-
- protected static final String HEADER_KEY_IF_NONE_MATCH = "If-None-Match";
- protected static final String HEADER_KEY_ETAG = "ETag";
-
- private static final String API_PATH = "/ocs/v2.php/cloud/capabilities";
- private static final String METHOD_GET = "GET";
- private static final String PARAM_KEY_FORMAT = "format";
- private static final String PARAM_VALUE_JSON = "json";
-
- private static final Map<String, String> parameters = new HashMap<>();
-
- static {
- parameters.put(PARAM_KEY_FORMAT, PARAM_VALUE_JSON);
- }
-
- public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag) throws Exception {
- final NextcloudRequest.Builder requestBuilder = new NextcloudRequest.Builder()
- .setMethod(METHOD_GET)
- .setUrl(API_PATH)
- .setParameter(parameters);
-
- final Map<String, List<String>> header = new HashMap<>();
- if (lastETag != null && !lastETag.isEmpty()) {
- header.put(HEADER_KEY_IF_NONE_MATCH, Collections.singletonList('"' + lastETag + '"'));
- requestBuilder.setHeader(header);
- }
-
- final NextcloudRequest nextcloudRequest = requestBuilder.build();
- final StringBuilder result = new StringBuilder();
+ private static final String HEADER_KEY_ETAG = "ETag";
+ public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag) throws Throwable {
+ final OcsAPI ocsAPI = ApiProvider.getOcsAPI(context, ssoAccount);
try {
- Log.v(TAG, ssoAccount.name + " → " + nextcloudRequest.getMethod() + " " + nextcloudRequest.getUrl() + " ");
- final Response response = SSOClient.requestFilesApp(context.getApplicationContext(), ssoAccount, nextcloudRequest);
- Log.v(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
-
- final BufferedReader rd = new BufferedReader(new InputStreamReader(response.getBody()));
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- response.getBody().close();
-
- String etag = null;
- final AidlNetworkRequest.PlainHeader eTagHeader = response.getPlainHeader(HEADER_KEY_ETAG);
- if (eTagHeader != null) {
- etag = eTagHeader.getValue().replace("\"", "");
+ final ParsedResponse<Capabilities> response = ocsAPI.getCapabilities(lastETag).blockingSingle();
+ final Capabilities capabilities = response.getResponse();
+ final Map<String, String> headers = response.getHeaders();
+ if (headers != null) {
+ capabilities.setETag(headers.get(HEADER_KEY_ETAG));
+ } else {
+ Log.w(TAG, "Response headers of capabilities are null");
}
-
- return new Capabilities(result.toString(), etag);
- } catch (NullPointerException e) {
- final PackageInfo pInfo = context.getApplicationContext().getPackageManager().getPackageInfo("com.nextcloud.client", 0);
- if (pInfo.versionCode < MIN_NEXTCLOUD_FILES_APP_VERSION_CODE) {
- throw new NextcloudFilesAppNotSupportedException();
+ return capabilities;
+ } catch (RuntimeException e) {
+ final Throwable cause = e.getCause();
+ if(cause != null) {
+ throw cause;
} else {
throw e;
}
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 a9590225..1dff46cf 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
@@ -42,21 +42,24 @@ public class CapabilitiesWorker extends Worker {
@NonNull
@Override
public Result doWork() {
- final NotesDatabase db = NotesDatabase.getInstance(getApplicationContext());
- for (Account account : db.getAccountDao().getAccounts()) {
+ final NotesRepository repo = NotesRepository.getInstance(getApplicationContext());
+ for (Account account : repo.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.getAccountDao().updateCapabilitiesETag(account.getId(), capabilities.getETag());
- db.getAccountDao().updateBrand(account.getId(), capabilities.getColor(), capabilities.getTextColor());
- db.updateApiVersion(account.getId(), capabilities.getApiVersion());
+ repo.updateCapabilitiesETag(account.getId(), capabilities.getETag());
+ repo.updateBrand(account.getId(), capabilities.getColor(), capabilities.getTextColor());
+ repo.updateApiVersion(account.getId(), capabilities.getApiVersion());
Log.i(TAG, capabilities.toString());
- } catch (Exception e) {
+ } catch (Throwable e) {
if (e instanceof NextcloudHttpRequestFailedException) {
if (((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.i(TAG, "Capabilities not modified.");
return Result.success();
+ } else if(((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ Log.i(TAG, "Server is in maintenance mode.");
+ return Result.success();
}
}
e.printStackTrace();
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
deleted file mode 100644
index a03705e4..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java
+++ /dev/null
@@ -1,228 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import com.nextcloud.android.sso.aidl.NextcloudRequest;
-import com.nextcloud.android.sso.api.AidlNetworkRequest;
-import com.nextcloud.android.sso.api.Response;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-
-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;
-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.ServerResponse.NoteResponse;
-import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse;
-import it.niedermann.owncloud.notes.shared.model.ServerSettings;
-
-@SuppressWarnings("WeakerAccess")
-@WorkerThread
-public abstract class NotesClient {
-
- final static int MIN_NEXTCLOUD_FILES_APP_VERSION_CODE = 30090000;
- private static final String TAG = NotesClient.class.getSimpleName();
-
- protected final Context appContext;
-
- protected static final String GET_PARAM_KEY_PRUNE_BEFORE = "pruneBefore";
-
- protected static final String HEADER_KEY_ETAG = "ETag";
- protected static final String HEADER_KEY_LAST_MODIFIED = "Last-Modified";
- protected static final String HEADER_KEY_CONTENT_TYPE = "Content-Type";
- protected static final String HEADER_KEY_IF_NONE_MATCH = "If-None-Match";
- protected static final String HEADER_KEY_X_NOTES_API_VERSIONS = "X-Notes-API-Versions";
-
- protected static final String HEADER_VALUE_APPLICATION_JSON = "application/json";
-
- protected static final String METHOD_GET = "GET";
- protected static final String METHOD_PUT = "PUT";
- protected static final String METHOD_POST = "POST";
- protected static final String METHOD_DELETE = "DELETE";
-
- public static final String JSON_ID = "id";
- public static final String JSON_TITLE = "title";
- public static final String JSON_CONTENT = "content";
- public static final String JSON_FAVORITE = "favorite";
- public static final String JSON_CATEGORY = "category";
- public static final String JSON_ETAG = "etag";
- public static final String JSON_MODIFIED = "modified";
- public static final String JSON_SETTINGS_NOTES_PATH = "notesPath";
- public static final String JSON_SETTINGS_FILE_SUFFIX = "fileSuffix";
-
- public static final ApiVersion[] SUPPORTED_API_VERSIONS = new ApiVersion[]{
- new ApiVersion(1, 0),
- new ApiVersion(0, 2)
- };
-
- public static NotesClient newInstance(@Nullable ApiVersion preferredApiVersion,
- @NonNull Context appContext) {
- if (preferredApiVersion == null) {
- Log.i(TAG, "apiVersion is null, using " + NotesClientV02.class.getSimpleName());
- return new NotesClientV02(appContext);
- } else if (preferredApiVersion.compareTo(SUPPORTED_API_VERSIONS[0]) == 0) {
- Log.i(TAG, "Using " + NotesClientV1.class.getSimpleName());
- return new NotesClientV1(appContext);
- } else if (preferredApiVersion.compareTo(SUPPORTED_API_VERSIONS[1]) == 0) {
- Log.i(TAG, "Using " + NotesClientV02.class.getSimpleName());
- return new NotesClientV02(appContext);
- }
- Log.w(TAG, "Unsupported API version " + preferredApiVersion + " - try using " + NotesClientV02.class.getSimpleName());
- return new NotesClientV02(appContext);
- }
-
- @SuppressWarnings("WeakerAccess")
- protected NotesClient(@NonNull Context appContext) {
- this.appContext = appContext;
- }
-
- /**
- * 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, Note note) throws Exception;
-
- abstract NoteResponse editNote(SingleSignOnAccount ssoAccount, Note note) throws Exception;
-
- abstract void deleteNote(SingleSignOnAccount ssoAccount, long noteId) throws Exception;
-
- public ServerSettings getServerSettings(SingleSignOnAccount ssoAccount) throws Exception {
- throw new UnsupportedOperationException("Not available in this API version");
- }
-
- public ServerSettings putServerSettings(SingleSignOnAccount ssoAccount, @NonNull ServerSettings settings) throws Exception {
- throw new UnsupportedOperationException("Not available in this API version");
- }
-
- /**
- * This entity class is used to return relevant data of the HTTP reponse.
- */
- public static class ResponseData {
- private final String content;
- private final String etag;
- private final String supportedApiVersions;
- private final Calendar lastModified;
-
- ResponseData(@NonNull String content, String etag, @NonNull Calendar lastModified, @Nullable String supportedApiVersions) {
- this.content = content;
- this.etag = etag;
- this.lastModified = lastModified;
- this.supportedApiVersions = supportedApiVersions;
- }
-
- public String getContent() {
- return content;
- }
-
- public String getETag() {
- return etag;
- }
-
- public Calendar getLastModified() {
- return lastModified;
- }
-
- public String getSupportedApiVersions() {
- return this.supportedApiVersions;
- }
- }
-
- abstract protected String getApiPath();
-
- /**
- * Request-Method for POST, PUT with or without JSON-Object-Parameter
- *
- * @param target Filepath to the wanted function
- * @param method GET, POST, DELETE or PUT
- * @param parameter optional headers to be sent
- * @param requestBody JSON Object which shall be transferred to the server.
- * @param lastETag optional ETag of last response
- * @return Body of answer
- */
- protected ResponseData requestServer(SingleSignOnAccount ssoAccount, String target, String method, Map<String, String> parameter, JSONObject requestBody, String lastETag) throws Exception {
- final NextcloudRequest.Builder requestBuilder = new NextcloudRequest.Builder()
- .setMethod(method)
- .setUrl(getApiPath() + target);
- if (parameter != null) {
- requestBuilder.setParameter(parameter);
- }
-
- final Map<String, List<String>> header = new HashMap<>();
- if (requestBody != null) {
- header.put(HEADER_KEY_CONTENT_TYPE, Collections.singletonList(HEADER_VALUE_APPLICATION_JSON));
- requestBuilder.setRequestBody(requestBody.toString());
- }
- if (lastETag != null && !lastETag.isEmpty() && METHOD_GET.equals(method)) {
- header.put(HEADER_KEY_IF_NONE_MATCH, Collections.singletonList('"' + lastETag + '"'));
- requestBuilder.setHeader(header);
- }
-
- final NextcloudRequest nextcloudRequest = requestBuilder.build();
- final StringBuilder result = new StringBuilder();
-
- try {
- Log.v(TAG, ssoAccount.name + " → " + nextcloudRequest.getMethod() + " " + nextcloudRequest.getUrl() + " ");
- Log.d(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
- final Response response = SSOClient.requestFilesApp(appContext, ssoAccount, nextcloudRequest);
-
- final BufferedReader rd = new BufferedReader(new InputStreamReader(response.getBody()));
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- response.getBody().close();
-
- String etag = "";
- final AidlNetworkRequest.PlainHeader eTagHeader = response.getPlainHeader(HEADER_KEY_ETAG);
- if (eTagHeader != null) {
- etag = Objects.requireNonNull(eTagHeader.getValue()).replace("\"", "");
- }
-
- final Calendar lastModified = Calendar.getInstance();
- lastModified.setTimeInMillis(0);
- final AidlNetworkRequest.PlainHeader lastModifiedHeader = response.getPlainHeader(HEADER_KEY_LAST_MODIFIED);
- if (lastModifiedHeader != null)
- lastModified.setTimeInMillis(Date.parse(lastModifiedHeader.getValue()));
- Log.d(TAG, "ETag: " + etag + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
-
- String supportedApiVersions = null;
- final AidlNetworkRequest.PlainHeader supportedApiVersionsHeader = response.getPlainHeader(HEADER_KEY_X_NOTES_API_VERSIONS);
- if (supportedApiVersionsHeader != null) {
- supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader.getValue()) + "]";
- }
-
- // return these header fields since they should only be saved after successful processing the result!
- return new ResponseData(result.toString(), etag, lastModified, supportedApiVersions);
- } catch (NullPointerException e) {
- final PackageInfo pInfo = appContext.getPackageManager().getPackageInfo("com.nextcloud.client", 0);
- if (pInfo.versionCode < MIN_NEXTCLOUD_FILES_APP_VERSION_CODE) {
- throw new NextcloudFilesAppNotSupportedException();
- } else {
- throw e;
- }
- }
- }
-}
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
deleted file mode 100644
index 31a4de39..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-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.persistence.entity.Note;
-import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse;
-import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse;
-
-@WorkerThread
-public class NotesClientV02 extends NotesClient {
-
- private static final String API_PATH = "/index.php/apps/notes/api/v0.2/";
-
- NotesClientV02(@NonNull Context appContext) {
- super(appContext);
- }
-
- 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, 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() / 1_000);
- paramObject.accumulate(JSON_FAVORITE, note.getFavorite());
- paramObject.accumulate(JSON_CATEGORY, note.getCategory());
- return new NoteResponse(requestServer(ssoAccount, path, method, null, paramObject, null));
- }
-
- @Override
- NoteResponse createNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
- return putNote(ssoAccount, note, "notes", METHOD_POST);
- }
-
- @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);
- }
-
- @Override
- protected String getApiPath() {
- return API_PATH;
- }
-}
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
deleted file mode 100644
index 614d99b5..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-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.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;
-
-@WorkerThread
-public class NotesClientV1 extends NotesClient {
-
- private static final String API_PATH = "/index.php/apps/notes/api/v1/";
-
- NotesClientV1(@NonNull Context appContext) {
- super(appContext);
- }
-
- 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, 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() / 1_000);
- paramObject.accumulate(JSON_FAVORITE, note.getFavorite());
- paramObject.accumulate(JSON_CATEGORY, note.getCategory());
- return new NoteResponse(requestServer(ssoAccount, path, method, null, paramObject, null));
- }
-
- @Override
- NoteResponse createNote(SingleSignOnAccount ssoAccount, Note note) throws Exception {
- return putNote(ssoAccount, note, "notes", METHOD_POST);
- }
-
- @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);
- }
-
- @Override
- protected String getApiPath() {
- return API_PATH;
- }
-
- @Override
- public ServerSettings getServerSettings(SingleSignOnAccount ssoAccount) throws Exception {
- return ServerSettings.from(new JSONObject(this.requestServer(ssoAccount, "settings", METHOD_GET, null, null, null).getContent()));
- }
-
- @Override
- public ServerSettings putServerSettings(SingleSignOnAccount ssoAccount, @NonNull ServerSettings settings) throws Exception {
- final JSONObject paramObject = new JSONObject();
- paramObject.accumulate(JSON_SETTINGS_NOTES_PATH, settings.getNotesPath());
- paramObject.accumulate(JSON_SETTINGS_FILE_SUFFIX, settings.getFileSuffix());
- return ServerSettings.from(new JSONObject(this.requestServer(ssoAccount, "settings", METHOD_PUT, null, paramObject, null).getContent()));
- }
-}
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 40d918df..4f4aaa78 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,43 +1,15 @@
package it.niedermann.owncloud.notes.persistence;
import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
-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.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 org.json.JSONArray;
-import org.json.JSONException;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
-import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.persistence.dao.AccountDao;
import it.niedermann.owncloud.notes.persistence.dao.CategoryOptionsDao;
import it.niedermann.owncloud.notes.persistence.dao.NoteDao;
@@ -61,24 +33,6 @@ 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.CategorySortingMethod;
-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.NavigationCategory;
-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 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.singlenote.SingleNoteWidget.updateSingleNoteWidgets;
-import static java.util.stream.Collectors.toMap;
@Database(
entities = {
@@ -94,13 +48,16 @@ public abstract class NotesDatabase extends RoomDatabase {
private static final String TAG = NotesDatabase.class.getSimpleName();
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 static volatile NotesDatabase instance;
+
+ public static NotesDatabase getInstance(@NonNull Context context) {
+ if (instance == null) {
+ instance = create(context.getApplicationContext());
+ }
+ return instance;
+ }
private static NotesDatabase create(final Context context) {
- defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", context);
return Room.databaseBuilder(
context,
NotesDatabase.class,
@@ -110,9 +67,9 @@ public abstract class NotesDatabase extends RoomDatabase {
new Migration_10_11(context),
new Migration_11_12(context),
new Migration_12_13(context),
- new Migration_13_14(context, () -> instance.notifyWidgets()),
+ new Migration_13_14(context),
new Migration_14_15(),
- new Migration_15_16(context, () -> instance.notifyWidgets()),
+ new Migration_15_16(context),
new Migration_16_17(),
new Migration_17_18(),
new Migration_18_19(context),
@@ -144,407 +101,4 @@ public abstract class NotesDatabase extends RoomDatabase {
public abstract WidgetSingleNoteDao getWidgetSingleNoteDao();
public abstract WidgetNotesListDao getWidgetNotesListDao();
-
- public static NotesDatabase getInstance(@NonNull Context context) {
- if (instance == null) {
- instance = create(context.getApplicationContext());
- NotesDatabase.context = context.getApplicationContext();
- NotesDatabase.serverSyncHelper = NotesServerSyncHelper.getInstance(instance);
- }
- return instance;
- }
-
- public NotesServerSyncHelper getNoteServerSyncHelper() {
- return NotesDatabase.serverSyncHelper;
- }
-
- /**
- * Creates a new Note in the Database and adds a Synchronization Flag.
- *
- * @param note Note
- */
- @NonNull
- @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;
- });
- }
-
- /**
- * 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)}!
- *
- * @param note {@link Note} to be added.
- */
- @NonNull
- @WorkerThread
- 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));
- }
-
- @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);
- });
- }
-
- /**
- * @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 Map<Long, Long> getIdMap(long accountId) {
- validateAccountId(accountId);
- return getNoteDao()
- .getRemoteIdAndId(accountId)
- .stream()
- .filter(note -> note.getRemoteId() != null)
- .collect(toMap(Note::getRemoteId, Note::getId));
- }
-
- @AnyThread
- public void toggleFavoriteAndSync(Account account, long noteId) {
- new Thread(() -> {
- getNoteDao().toggleFavorite(noteId);
- serverSyncHelper.scheduleSync(account, true);
- }).start();
- }
-
- /**
- * Set the category for a given note.
- * 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 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.
- */
- @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();
- }
-
- /**
- * Updates a single Note with a new content.
- * The title is derived from the new content automatically, and modified date as well as DBStatus are updated, too -- if the content differs to the state in the database.
- *
- * @param oldNote Note to be changed
- * @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 Note} if differs from database, otherwise the old {@link Note}.
- */
- @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 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() == 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 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());
- }
- 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(localAccount, callback);
- }
- serverSyncHelper.scheduleSync(localAccount, true);
- return newNote;
- } else {
- if (callback != null) {
- callback.onFinish();
- }
- return oldNote;
- }
- }
-
- /**
- * 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
- */
- @AnyThread
- public void deleteNoteAndSync(Account account, long id) {
- new Thread(() -> {
- getNoteDao().updateStatus(id, DBStatus.LOCAL_DELETED);
- notifyWidgets();
- serverSyncHelper.scheduleSync(account, true);
-
- 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.");
- }
- }
- }).start();
- }
-
- /**
- * Notify about changed notes.
- */
- @AnyThread
- protected void notifyWidgets() {
- new Thread(() -> {
- updateSingleNoteWidgets(context);
- updateNoteListWidgets(context);
- }).start();
- }
-
- @AnyThread
- void updateDynamicShortcuts(long accountId) {
- new Thread(() -> {
- 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 (Note note : getNoteDao().getRecentNotes(accountId)) {
- if (!TextUtils.isEmpty(note.getTitle())) {
- 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(context.getApplicationContext(), note.getId() + "")
- .setShortLabel(note.getTitle() + "")
- .setIcon(Icon.createWithResource(context.getApplicationContext(), note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
- .setIntent(intent)
- .build());
- } else {
- // Prevent crash https://github.com/stefan-niedermann/nextcloud-notes/issues/613
- Log.e(TAG, "shortLabel cannot be empty " + note);
- }
- }
- Log.d(TAG, "Update dynamic shortcuts");
- shortcutManager.removeAllDynamicShortcuts();
- shortcutManager.addDynamicShortcuts(newShortcuts);
- }
- }
- }
- }).start();
- }
-
- @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)));
- }
-
- /**
- * @param apiVersion has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
- * @return whether or not the given {@link ApiVersion} has been written to the database
- * @throws IllegalArgumentException if the apiVersion does not match the expected format
- */
- public boolean updateApiVersion(long accountId, @Nullable String apiVersion) throws IllegalArgumentException {
- validateAccountId(accountId);
- if (apiVersion != null) {
- try {
- JSONArray apiVersions = new JSONArray(apiVersion);
- for (int i = 0; i < apiVersions.length(); i++) {
- ApiVersion.of(apiVersions.getString(i));
- }
- if (apiVersions.length() > 0) {
- final int updatedRows = getAccountDao().updateApiVersion(accountId, apiVersion);
- if (updatedRows == 1) {
- 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 + "\"");
- }
- return true;
- } else {
- Log.i(TAG, "Given API version is a valid JSON array but does not contain any valid API versions. Do not update database.");
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("API version does contain a non-valid version: " + apiVersion);
- } catch (JSONException e) {
- throw new IllegalArgumentException("API version must contain be a JSON array: " + apiVersion);
- }
- } else {
- Log.v(TAG, "Given API version is null. Do not update database");
- }
- return false;
- }
-
- /**
- * @param localAccount the {@link Account} that should be deleted
- * @throws IllegalArgumentException if no account has been deleted by the given accountId
- */
- @AnyThread
- public LiveData<Void> deleteAccount(@NonNull Account localAccount) throws IllegalArgumentException {
- validateAccountId(localAccount.getId());
- 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");
- }
-
- try {
- SSOClient.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName()));
- } catch (NextcloudFilesAppAccountNotFoundException e) {
- e.printStackTrace();
- SSOClient.invalidateAPICache();
- }
-
- // 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) {
- if (accountId < 1) {
- throw new IllegalArgumentException("accountId must be greater than 0");
- }
- }
-
- /**
- * 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 selectedCategory The category to be modified
- * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
- */
- @AnyThread
- public void modifyCategoryOrder(long accountId, @NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
- validateAccountId(accountId);
-
- 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 {@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 selectedCategory The category
- * @return The sorting method in CategorySortingMethod enum format
- */
- @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);
- }
- }
- }
-
- return map(new SharedPreferenceIntLiveData(sp, prefKey, CategorySortingMethod.SORT_MODIFIED_DESC.getId()), CategorySortingMethod::findById);
- }
-
- public Context getContext() {
- return NotesDatabase.context;
- }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
new file mode 100644
index 00000000..afd6145d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
@@ -0,0 +1,930 @@
+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.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.ColorInt;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+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 org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+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.android.sharedpreferences.SharedPreferenceIntLiveData;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.edit.EditNoteActivity;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryOptions;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+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.shared.model.ApiVersion;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
+import it.niedermann.owncloud.notes.shared.model.NotesSettings;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+import it.niedermann.owncloud.notes.shared.util.SSOUtil;
+import retrofit2.Call;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.O;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+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.singlenote.SingleNoteWidget.updateSingleNoteWidgets;
+import static java.util.stream.Collectors.toMap;
+
+@SuppressWarnings("UnusedReturnValue")
+public class NotesRepository {
+
+ private static final String TAG = NotesRepository.class.getSimpleName();
+
+ private static NotesRepository instance;
+
+ private final ExecutorService executor;
+ private final Context context;
+ private final NotesDatabase db;
+ private final String defaultNonEmptyTitle;
+
+ /**
+ * 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)) {
+ executor.submit(() -> {
+ try {
+ scheduleSync(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.");
+ }
+ });
+ }
+ }
+ };
+
+ // 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<>();
+
+
+ public static synchronized NotesRepository getInstance(@NonNull Context context) {
+ if (instance == null) {
+ instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool());
+ }
+ return instance;
+ }
+
+ private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor) {
+ this.context = context.getApplicationContext();
+ this.db = db;
+ this.executor = executor;
+ this.defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", this.context);
+ 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));
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
+ prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
+ syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
+
+ updateNetworkStatus();
+ }
+
+
+ // Accounts
+
+ @AnyThread
+ public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
+ final Account createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, capabilities)));
+ if (createdAccount == null) {
+ callback.onError(new Exception("Could not read created account."));
+ } else {
+ callback.onSuccess(createdAccount);
+ }
+ }
+
+ @WorkerThread
+ public List<Account> getAccounts() {
+ return db.getAccountDao().getAccounts();
+ }
+
+ @WorkerThread
+ public void deleteAccount(@NonNull Account account) {
+ try {
+ ApiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName()));
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ e.printStackTrace();
+ ApiProvider.invalidateAPICache();
+ }
+
+ db.getAccountDao().deleteAccount(account);
+ }
+
+ public Account getAccountByName(String accountName) {
+ return db.getAccountDao().getAccountByName(accountName);
+ }
+
+ public Account getAccountById(long accountId) {
+ return db.getAccountDao().getAccountById(accountId);
+ }
+
+ public LiveData<List<Account>> getAccounts$() {
+ return db.getAccountDao().getAccounts$();
+ }
+
+ public LiveData<Account> getAccountById$(long accountId) {
+ return db.getAccountDao().getAccountById$(accountId);
+ }
+
+ public LiveData<Integer> countAccounts$() {
+ return db.getAccountDao().countAccounts$();
+ }
+
+ public void updateBrand(long id, @ColorInt Integer color, @ColorInt Integer textColor) {
+ db.getAccountDao().updateBrand(id, color, textColor);
+ }
+
+ public void updateETag(long id, String eTag) {
+ db.getAccountDao().updateETag(id, eTag);
+ }
+
+ public void updateCapabilitiesETag(long id, String capabilitiesETag) {
+ db.getAccountDao().updateCapabilitiesETag(id, capabilitiesETag);
+ }
+
+ public void updateModified(long id, long modified) {
+ db.getAccountDao().updateModified(id, modified);
+ }
+
+
+ // Notes
+
+ public LiveData<Note> getNoteById$(long id) {
+ return db.getNoteDao().getNoteById$(id);
+ }
+
+ public Note getNoteById(long id) {
+ return db.getNoteDao().getNoteById(id);
+ }
+
+ public LiveData<Integer> count$(long accountId) {
+ return db.getNoteDao().count$(accountId);
+ }
+
+ public LiveData<Integer> countFavorites$(long accountId) {
+ return db.getNoteDao().countFavorites$(accountId);
+ }
+
+ public void updateScrollY(long id, int scrollY) {
+ db.getNoteDao().updateScrollY(id, scrollY);
+ }
+
+ public LiveData<List<CategoryWithNotesCount>> searchCategories$(Long accountId, String searchTerm) {
+ return db.getNoteDao().searchCategories$(accountId, searchTerm);
+ }
+
+ public LiveData<List<Note>> searchRecentByModified$(long accountId, String query) {
+ return db.getNoteDao().searchRecentByModified$(accountId, query);
+ }
+
+ public List<Note> searchRecentByModified(long accountId, String query) {
+ return db.getNoteDao().searchRecentByModified(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchRecentLexicographically$(long accountId, String query) {
+ return db.getNoteDao().searchRecentLexicographically$(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchFavoritesByModified$(long accountId, String query) {
+ return db.getNoteDao().searchFavoritesByModified$(accountId, query);
+ }
+
+ public List<Note> searchFavoritesByModified(long accountId, String query) {
+ return db.getNoteDao().searchFavoritesByModified(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchFavoritesLexicographically$(long accountId, String query) {
+ return db.getNoteDao().searchFavoritesLexicographically$(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchUncategorizedByModified$(long accountId, String query) {
+ return db.getNoteDao().searchUncategorizedByModified$(accountId, query);
+ }
+
+ public List<Note> searchUncategorizedByModified(long accountId, String query) {
+ return db.getNoteDao().searchUncategorizedByModified(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchUncategorizedLexicographically$(long accountId, String query) {
+ return db.getNoteDao().searchUncategorizedLexicographically$(accountId, query);
+ }
+
+ public LiveData<List<Note>> searchCategoryByModified$(long accountId, String query, String category) {
+ return db.getNoteDao().searchCategoryByModified$(accountId, query, category);
+ }
+
+ public List<Note> searchCategoryByModified(long accountId, String query, String category) {
+ return db.getNoteDao().searchCategoryByModified(accountId, query, category);
+ }
+
+ public LiveData<List<Note>> searchCategoryLexicographically$(long accountId, String query, String category) {
+ return db.getNoteDao().searchCategoryLexicographically$(accountId, query, category);
+ }
+
+ public LiveData<List<CategoryWithNotesCount>> getCategories$(Long accountId) {
+ return db.getNoteDao().getCategories$(accountId);
+ }
+
+ public void updateRemoteId(long id, Long remoteId) {
+ db.getNoteDao().updateRemoteId(id, remoteId);
+ }
+
+ public Long getLocalIdByRemoteId(long accountId, long remoteId) {
+ return db.getNoteDao().getLocalIdByRemoteId(accountId, remoteId);
+ }
+
+ public List<Note> getLocalModifiedNotes(long accountId) {
+ return db.getNoteDao().getLocalModifiedNotes(accountId);
+ }
+
+ public void deleteByNoteId(long id, DBStatus forceDBStatus) {
+ db.getNoteDao().deleteByNoteId(id, forceDBStatus);
+ }
+
+ /**
+ * Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
+ */
+ public int updateIfNotModifiedLocallyDuringSync(long noteId, Long targetModified, String targetTitle, boolean targetFavorite, String targetETag, String targetContent, String targetExcerpt, String contentBeforeSyncStart, String categoryBeforeSyncStart, boolean favoriteBeforeSyncStart) {
+ return db.getNoteDao().updateIfNotModifiedLocallyDuringSync(noteId, targetModified, targetTitle, targetFavorite, targetETag, targetContent, targetExcerpt, contentBeforeSyncStart, categoryBeforeSyncStart, favoriteBeforeSyncStart);
+ }
+
+ public int updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(long id, Long modified, String title, boolean favorite, String category, String eTag, String content, String excerpt) {
+ return db.getNoteDao().updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(id, modified, title, favorite, category, eTag, content, excerpt);
+ }
+
+ public long countUnsynchronizedNotes(long accountId) {
+ final Long unsynchronizedNotesCount = db.getNoteDao().countUnsynchronizedNotes(accountId);
+ return unsynchronizedNotesCount == null ? 0 : unsynchronizedNotesCount;
+ }
+
+
+ // SingleNoteWidget
+
+ public void createOrUpdateSingleNoteWidgetData(SingleNoteWidgetData data) {
+ db.getWidgetSingleNoteDao().createOrUpdateSingleNoteWidgetData(data);
+ }
+
+ public void removeSingleNoteWidget(int id) {
+ db.getWidgetSingleNoteDao().removeSingleNoteWidget(id);
+ }
+
+ public SingleNoteWidgetData getSingleNoteWidgetData(int id) {
+ return db.getWidgetSingleNoteDao().getSingleNoteWidgetData(id);
+ }
+
+
+ // ListWidget
+
+ public void createOrUpdateNoteListWidgetData(NotesListWidgetData data) {
+ db.getWidgetNotesListDao().createOrUpdateNoteListWidgetData(data);
+ }
+
+ public void removeNoteListWidget(int appWidgetId) {
+ db.getWidgetNotesListDao().removeNoteListWidget(appWidgetId);
+ }
+
+ public NotesListWidgetData getNoteListWidgetData(int appWidgetId) {
+ return db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId);
+ }
+
+ /**
+ * Creates a new Note in the Database and adds a Synchronization Flag.
+ *
+ * @param note Note
+ */
+ @NonNull
+ @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<>();
+ executor.submit(() -> ret.postValue(addNote(account.getId(), entity)));
+ return map(ret, newNote -> {
+ notifyWidgets();
+ scheduleSync(account, true);
+ return newNote;
+ });
+ }
+
+ /**
+ * 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)}!
+ *
+ * @param note {@link Note} to be added.
+ */
+ @NonNull
+ @WorkerThread
+ 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 db.getNoteDao().getNoteById(db.getNoteDao().addNote(note));
+ }
+
+ @MainThread
+ public LiveData<Note> moveNoteToAnotherAccount(Account account, @NonNull Note note) {
+ return switchMap(db.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);
+ });
+ }
+
+ /**
+ * @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 Map<Long, Long> getIdMap(long accountId) {
+ return db.getNoteDao()
+ .getRemoteIdAndId(accountId)
+ .stream()
+ .collect(toMap(Note::getRemoteId, Note::getId));
+ }
+
+ @AnyThread
+ public void toggleFavoriteAndSync(Account account, long noteId) {
+ executor.submit(() -> {
+ db.getNoteDao().toggleFavorite(noteId);
+ scheduleSync(account, true);
+ });
+ }
+
+ /**
+ * Set the category for a given note.
+ * 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 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.
+ */
+ @AnyThread
+ public void setCategory(@NonNull Account account, long noteId, @NonNull String category) {
+ executor.submit(() -> {
+ db.getNoteDao().updateStatus(noteId, DBStatus.LOCAL_EDITED);
+ db.getNoteDao().updateCategory(noteId, category);
+ scheduleSync(account, true);
+ });
+ }
+
+ /**
+ * Updates a single Note with a new content.
+ * The title is derived from the new content automatically, and modified date as well as DBStatus are updated, too -- if the content differs to the state in the database.
+ *
+ * @param oldNote Note to be changed
+ * @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 Note} if differs from database, otherwise the old {@link Note}.
+ */
+ @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 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() == null || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) < 0) &&
+ (defaultNonEmptyTitle.equals(oldNote.getTitle()))) {
+ title = NoteUtil.generateNonEmptyNoteTitle(newContent, context);
+ } else {
+ title = oldNote.getTitle();
+ }
+ }
+ 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());
+ }
+ int rows = db.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) {
+ addCallbackPush(localAccount, callback);
+ }
+ scheduleSync(localAccount, true);
+ return newNote;
+ } else {
+ if (callback != null) {
+ callback.onFinish();
+ }
+ return oldNote;
+ }
+ }
+
+ /**
+ * 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
+ */
+ @AnyThread
+ public void deleteNoteAndSync(Account account, long id) {
+ executor.submit(() -> {
+ db.getNoteDao().updateStatus(id, DBStatus.LOCAL_DELETED);
+ notifyWidgets();
+ scheduleSync(account, true);
+
+ 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.");
+ }
+ }
+ });
+ }
+
+ /**
+ * Notify about changed notes.
+ */
+ @AnyThread
+ private void notifyWidgets() {
+ executor.submit(() -> {
+ updateSingleNoteWidgets(context);
+ updateNoteListWidgets(context);
+ });
+ }
+
+ @AnyThread
+ private void updateDynamicShortcuts(long accountId) {
+ executor.submit(() -> {
+ 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 (Note note : db.getNoteDao().getRecentNotes(accountId)) {
+ if (!TextUtils.isEmpty(note.getTitle())) {
+ 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(context.getApplicationContext(), note.getId() + "")
+ .setShortLabel(note.getTitle() + "")
+ .setIcon(Icon.createWithResource(context.getApplicationContext(), note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
+ .setIntent(intent)
+ .build());
+ } else {
+ // Prevent crash https://github.com/stefan-niedermann/nextcloud-notes/issues/613
+ Log.e(TAG, "shortLabel cannot be empty " + note);
+ }
+ }
+ Log.d(TAG, "Update dynamic shortcuts");
+ shortcutManager.removeAllDynamicShortcuts();
+ shortcutManager.addDynamicShortcuts(newShortcuts);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * @param apiVersion has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
+ * @return whether or not the given {@link ApiVersion} has been written to the database
+ * @throws IllegalArgumentException if the apiVersion does not match the expected format
+ */
+ public boolean updateApiVersion(long accountId, @Nullable String apiVersion) throws IllegalArgumentException {
+ if (apiVersion != null) {
+ try {
+ JSONArray apiVersions = new JSONArray(apiVersion);
+ for (int i = 0; i < apiVersions.length(); i++) {
+ ApiVersion.of(apiVersions.getString(i));
+ }
+ if (apiVersions.length() > 0) {
+ final int updatedRows = db.getAccountDao().updateApiVersion(accountId, apiVersion);
+ if (updatedRows == 0) {
+ Log.d(TAG, "ApiVersion not updated, because it did not change");
+ } else if (updatedRows == 1) {
+ Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId);
+ ApiProvider.invalidateAPICache();
+ } else {
+ Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
+ }
+ return true;
+ } else {
+ Log.i(TAG, "Given API version is a valid JSON array but does not contain any valid API versions. Do not update database.");
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("API version does contain a non-valid version: " + apiVersion);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException("API version must contain be a JSON array: " + apiVersion);
+ }
+ } else {
+ Log.v(TAG, "Given API version is null. Do not update database");
+ }
+ return false;
+ }
+
+ /**
+ * 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 selectedCategory The category to be modified
+ * @param sortingMethod The sorting method in {@link CategorySortingMethod} enum format
+ */
+ @AnyThread
+ public void modifyCategoryOrder(long accountId, @NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
+ executor.submit(() -> {
+ 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 (db.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);
+ db.getCategoryOptionsDao().addCategoryOptions(categoryOptions);
+ }
+ } else {
+ throw new IllegalStateException("Tried to modify category order for " + ENavigationCategoryType.DEFAULT_CATEGORY + "but category is null.");
+ }
+ break;
+ }
+ }
+ sp.apply();
+ });
+ }
+
+ /**
+ * 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 selectedCategory The category
+ * @return The sorting method in CategorySortingMethod enum format
+ */
+ @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 db.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);
+ }
+ }
+ }
+
+ return map(new SharedPreferenceIntLiveData(sp, prefKey, CategorySortingMethod.SORT_MODIFIED_DESC.getId()), CategorySortingMethod::findById);
+ }
+
+ @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.
+ */
+ private 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 synchronized 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)) {
+ syncActive.put(account.getId(), true);
+ try {
+ Log.d(TAG, "... starting now");
+ final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, 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);
+ }
+ }
+
+ @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();
+ }
+ }
+ notifyWidgets();
+ 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) {
+ Log.i(TAG, e.getMessage());
+ networkConnected = false;
+ isSyncPossible = false;
+ }
+ }
+
+ @NonNull
+ public LiveData<Boolean> getSyncStatus() {
+ return distinctUntilChanged(this.syncStatus);
+ }
+
+ @NonNull
+ public LiveData<ArrayList<Throwable>> getSyncErrors() {
+ return this.syncErrors;
+ }
+
+ public Call<NotesSettings> getServerSettings(@NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) {
+ return ApiProvider.getNotesAPI(context, ssoAccount, preferredApiVersion).getSettings();
+ }
+
+ public Call<NotesSettings> putServerSettings(@NonNull SingleSignOnAccount ssoAccount, @NonNull NotesSettings settings, @Nullable ApiVersion preferredApiVersion) {
+ return ApiProvider.getNotesAPI(context, ssoAccount, preferredApiVersion).putSettings(settings);
+ }
+}
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
deleted file mode 100644
index e9fab1b0..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java
+++ /dev/null
@@ -1,346 +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.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
index ddc05981..88fb44f3 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
@@ -1,31 +1,40 @@
package it.niedermann.owncloud.notes.persistence;
+import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
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.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
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 retrofit2.Response;
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;
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
/**
@@ -36,10 +45,15 @@ abstract class NotesServerSyncTask extends Thread {
private static final String TAG = NotesServerSyncTask.class.getSimpleName();
+ private static final String HEADER_KEY_X_NOTES_API_VERSIONS = "X-Notes-API-Versions";
+ private static final String HEADER_KEY_ETAG = "ETag";
+ private static final String HEADER_KEY_LAST_MODIFIED = "Last-Modified";
+
+ private NotesAPI notesAPI;
@NonNull
- private final NotesClient notesClient;
+ private final Context context;
@NonNull
- private final NotesDatabase db;
+ private final NotesRepository repo;
@NonNull
protected final Account localAccount;
@NonNull
@@ -50,12 +64,12 @@ abstract class NotesServerSyncTask extends Thread {
@NonNull
protected final ArrayList<Throwable> exceptions = new ArrayList<>();
- NotesServerSyncTask(@NonNull NotesClient notesClient, @NonNull NotesDatabase db, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
+ NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges) throws NextcloudFilesAppAccountNotFoundException {
super(TAG);
- this.notesClient = notesClient;
- this.db = db;
+ this.context = context;
+ this.repo = repo;
this.localAccount = localAccount;
- this.ssoAccount = ssoAccount;
+ this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName());
this.onlyLocalChanges = onlyLocalChanges;
}
@@ -67,12 +81,16 @@ abstract class NotesServerSyncTask extends Thread {
public void run() {
onPreExecute();
+ notesAPI = ApiProvider.getNotesAPI(context, ssoAccount, localAccount.getPreferredApiVersion());
+
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);
@@ -89,7 +107,7 @@ abstract class NotesServerSyncTask extends Thread {
Log.d(TAG, "pushLocalChanges()");
boolean success = true;
- final List<Note> notes = db.getNoteDao().getLocalModifiedNotes(localAccount.getId());
+ final List<Note> notes = repo.getLocalModifiedNotes(localAccount.getId());
for (Note note : notes) {
Log.d(TAG, " Process Local Note: " + note);
try {
@@ -99,41 +117,51 @@ abstract class NotesServerSyncTask extends Thread {
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) {
+ final Response<Note> editResponse = notesAPI.editNote(note).execute();
+ if (editResponse.isSuccessful()) {
+ remoteNote = editResponse.body();
+ } else {
+ if (editResponse.code() == HTTP_NOT_FOUND) {
Log.v(TAG, " ...Note does no longer exist on server → recreate");
- remoteNote = notesClient.createNote(ssoAccount, note).getNote();
+ final Response<Note> createResponse = notesAPI.createNote(note).execute();
+ if (createResponse.isSuccessful()) {
+ remoteNote = createResponse.body();
+ } else {
+ throw new Exception(createResponse.errorBody().string());
+ }
} else {
- throw e;
+ throw new Exception(editResponse.errorBody().string());
}
}
} 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());
+ final Response<Note> createResponse = notesAPI.createNote(note).execute();
+ if (createResponse.isSuccessful()) {
+ remoteNote = createResponse.body();
+ repo.updateRemoteId(note.getId(), remoteNote.getRemoteId());
+ } else {
+ throw new Exception(createResponse.errorBody().string());
+ }
}
// 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());
+ repo.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) {
+ final Response<Void> deleteResponse = notesAPI.deleteNote(note.getRemoteId()).execute();
+ if (!deleteResponse.isSuccessful()) {
+ if (deleteResponse.code() == HTTP_NOT_FOUND) {
Log.v(TAG, " ...delete (note has already been deleted remotely)");
} else {
- throw e;
+ throw new Exception(deleteResponse.errorBody().string());
}
}
}
// 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);
+ repo.deleteByNoteId(note.getId(), LOCAL_DELETED);
break;
default:
throw new IllegalStateException("Unknown State of Note " + note + ": " + note.getStatus());
@@ -147,7 +175,7 @@ abstract class NotesServerSyncTask extends Thread {
}
} catch (Exception e) {
if (e instanceof TokenMismatchException) {
- SSOClient.invalidateAPICache(ssoAccount);
+ ApiProvider.invalidateAPICache(ssoAccount);
}
exceptions.add(e);
success = false;
@@ -162,15 +190,19 @@ abstract class NotesServerSyncTask extends Thread {
private boolean pullRemoteChanges() {
Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
try {
- final Map<Long, Long> idMap = db.getIdMap(localAccount.getId());
+ final Map<Long, Long> idMap = repo.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());
+ final Account accountFromDatabase = repo.getAccountById(localAccount.getId());
+ if (accountFromDatabase == null) {
+ callbacks.remove(localAccount.getId());
+ return true;
+ }
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 ParsedResponse<List<Note>> fetchResponse = notesAPI.getNotes(localAccount.getModified(), localAccount.getETag()).blockingSingle();
+ final List<Note> remoteNotes = fetchResponse.getResponse();
final Set<Long> remoteIDs = new HashSet<>();
// pull remote changes: update or create each remote note
for (Note remoteNote : remoteNotes) {
@@ -182,14 +214,14 @@ abstract class NotesServerSyncTask extends Thread {
Log.v(TAG, " ... found → Update");
Long localId = idMap.get(remoteNote.getRemoteId());
if (localId != null) {
- db.getNoteDao().updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
+ repo.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);
+ repo.addNote(localAccount.getId(), remoteNote);
}
}
Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
@@ -197,36 +229,54 @@ abstract class NotesServerSyncTask extends Thread {
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);
+ repo.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());
+ localAccount.setETag(fetchResponse.getHeaders().get(HEADER_KEY_ETAG));
+
+ final Calendar lastModified = Calendar.getInstance();
+ lastModified.setTimeInMillis(0);
+ final String lastModifiedHeader = fetchResponse.getHeaders().get(HEADER_KEY_LAST_MODIFIED);
+ if (lastModifiedHeader != null)
+ lastModified.setTimeInMillis(Date.parse(lastModifiedHeader));
+ Log.d(TAG, "ETag: " + fetchResponse.getHeaders().get(HEADER_KEY_ETAG) + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
+
+ localAccount.setModified(lastModified);
+
+ repo.updateETag(localAccount.getId(), localAccount.getETag());
+ repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
+
+
+ String supportedApiVersions = null;
+ final String supportedApiVersionsHeader = fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS);
+ if (supportedApiVersionsHeader != null) {
+ supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader) + "]";
+ }
try {
- if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
- localAccount.setApiVersion(response.getSupportedApiVersions());
+ if (repo.updateApiVersion(localAccount.getId(), supportedApiVersions)) {
+ localAccount.setApiVersion(supportedApiVersions);
}
} 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);
+ } catch (Throwable t) {
+ final Throwable cause = t.getCause();
+ if (t.getClass() == RuntimeException.class && cause != null) {
+ if (cause.getClass() == NextcloudHttpRequestFailedException.class || cause instanceof NextcloudHttpRequestFailedException) {
+ final NextcloudHttpRequestFailedException httpException = (NextcloudHttpRequestFailedException) cause;
+ if (httpException.getStatusCode() == HTTP_NOT_MODIFIED) {
+ Log.d(TAG, "Server returned HTTP Status Code " + httpException.getStatusCode() + " - Notes not modified.");
+ return true;
+ } else if (httpException.getStatusCode() == HTTP_UNAVAILABLE) {
+ Log.d(TAG, "Server returned HTTP Status Code " + httpException.getStatusCode() + " - Server is in maintenance mode.");
+ return true;
+ }
+ }
}
- exceptions.add(e);
+ exceptions.add(t);
return false;
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SSOClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SSOClient.java
deleted file mode 100644
index d3976617..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SSOClient.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-import com.google.gson.GsonBuilder;
-import com.nextcloud.android.sso.aidl.NextcloudRequest;
-import com.nextcloud.android.sso.api.NextcloudAPI;
-import com.nextcloud.android.sso.api.Response;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@SuppressWarnings("WeakerAccess")
-@WorkerThread
-public class SSOClient {
-
- private static final String TAG = SSOClient.class.getSimpleName();
-
- private static final Map<String, NextcloudAPI> mNextcloudAPIs = new HashMap<>();
-
- public static Response requestFilesApp(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @NonNull NextcloudRequest nextcloudRequest) throws Exception {
- return getNextcloudAPI(context.getApplicationContext(), ssoAccount).performNetworkRequestV2(nextcloudRequest);
- }
-
- private static NextcloudAPI getNextcloudAPI(Context appContext, SingleSignOnAccount ssoAccount) {
- if (mNextcloudAPIs.containsKey(ssoAccount.name)) {
- return mNextcloudAPIs.get(ssoAccount.name);
- } else {
- Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
- final NextcloudAPI nextcloudAPI = new NextcloudAPI(appContext, ssoAccount, new GsonBuilder().create(), new NextcloudAPI.ApiConnectedListener() {
- @Override
- public void onConnected() {
- Log.i(TAG, "SSO API connected for " + ssoAccount);
- }
-
- @Override
- public void onError(Exception ex) {
- ex.printStackTrace();
- }
- });
- mNextcloudAPIs.put(ssoAccount.name, nextcloudAPI);
- return nextcloudAPI;
- }
- }
-
- /**
- * Invalidates thes API cache for the given ssoAccount
- *
- * @param ssoAccount the ssoAccount for which the API cache should be cleared.
- */
- public static void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
- Log.v(TAG, "Invalidating API cache for " + ssoAccount.name);
- if (mNextcloudAPIs.containsKey(ssoAccount.name)) {
- final NextcloudAPI nextcloudAPI = mNextcloudAPIs.get(ssoAccount.name);
- if (nextcloudAPI != null) {
- nextcloudAPI.stop();
- }
- mNextcloudAPIs.remove(ssoAccount.name);
- }
- }
-
- /**
- * Invalidates the whole API cache for all accounts
- */
- public static void invalidateAPICache() {
- for (String key : mNextcloudAPIs.keySet()) {
- Log.v(TAG, "Invalidating API cache for " + key);
- if (mNextcloudAPIs.containsKey(key)) {
- final NextcloudAPI nextcloudAPI = mNextcloudAPIs.get(key);
- if (nextcloudAPI != null) {
- nextcloudAPI.stop();
- }
- mNextcloudAPIs.remove(key);
- }
- }
- }
-}
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 1b86e0aa..1d4a8bc7 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
@@ -34,11 +34,11 @@ public class SyncWorker extends Worker {
@NonNull
@Override
public Result doWork() {
- NotesDatabase db = NotesDatabase.getInstance(getApplicationContext());
- for (Account account : db.getAccountDao().getAccounts()) {
+ NotesRepository repo = NotesRepository.getInstance(getApplicationContext());
+ for (Account account : repo.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);
+ repo.addCallbackPull(account, () -> Log.v(TAG, "Finished background synchronization for " + account.getAccountName()));
+ repo.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
index b0828717..085c0a16 100644
--- 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
@@ -53,6 +53,6 @@ public interface AccountDao {
@Query("UPDATE Account SET MODIFIED = :modified WHERE id = :id")
void updateModified(long id, long modified);
- @Query("UPDATE Account SET APIVERSION = :apiVersion WHERE id = :id")
+ @Query("UPDATE Account SET APIVERSION = :apiVersion WHERE id = :id AND ((APIVERSION IS NULL AND :apiVersion IS NOT NULL) OR (APIVERSION IS NOT NULL AND :apiVersion IS NULL) OR APIVERSION <> :apiVersion)")
int updateApiVersion(Long id, String apiVersion);
}
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
index b0e93d2c..618dff37 100644
--- 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
@@ -10,7 +10,6 @@ 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;
@@ -139,7 +138,11 @@ public interface NoteDao {
@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'")
+ /**
+ * Gets a list of {@link Note} objects with filled {@link Note#id} and {@link Note#remoteId},
+ * where {@link Note#remoteId} is not <code>null</code>
+ */
+ @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' AND remoteId IS NOT NULL")
List<Note> getRemoteIdAndId(long accountId);
/**
@@ -169,7 +172,7 @@ public interface NoteDao {
void updateRemoteId(long id, Long remoteId);
/**
- * used by: {@link NotesServerSyncHelper.SyncTask#pushLocalChanges()} update only, if not modified locally during the synchronization
+ * used by: {@link it.niedermann.owncloud.notes.persistence.NotesServerSyncTask#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 " +
@@ -177,7 +180,7 @@ public interface NoteDao {
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)
+ * used by: {@link it.niedermann.owncloud.notes.persistence.NotesServerSyncTask#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)")
@@ -194,4 +197,7 @@ public interface NoteDao {
@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);
+
+ @Query("SELECT COUNT(*) FROM NOTE WHERE STATUS != '' AND accountId = :accountId")
+ Long countUnsynchronizedNotes(long accountId);
}
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
index ed6dcd6c..09f3fc26 100644
--- 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
@@ -7,7 +7,6 @@ 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;
@@ -21,7 +20,6 @@ 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;
@@ -83,8 +81,8 @@ public class Account implements Serializable {
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) {
+ for (ApiVersion temp : ApiVersion.SUPPORTED_API_VERSIONS) {
+ if (temp.equals(parsedApiVersion)) {
supportedApiVersions.add(parsedApiVersion);
break;
}
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
index 7224d4eb..376c099d 100644
--- 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
@@ -9,6 +9,9 @@ import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
import java.io.Serializable;
import java.util.Calendar;
@@ -34,31 +37,52 @@ import it.niedermann.owncloud.notes.shared.model.Item;
}
)
public class Note implements Serializable, Item {
+ @SerializedName("localId")
@PrimaryKey(autoGenerate = true)
private long id;
+
@Nullable
+ @Expose
+ @SerializedName("id")
private Long remoteId;
+
private long accountId;
+
@NonNull
private DBStatus status = DBStatus.VOID;
+
@NonNull
@ColumnInfo(defaultValue = "")
+ @Expose
private String title = "";
+
@NonNull
+ @Expose
@ColumnInfo(defaultValue = "")
private String category = "";
+
+ @Expose
@Nullable
private Calendar modified;
+
@NonNull
@ColumnInfo(defaultValue = "")
+ @Expose
private String content = "";
+
+ @Expose
@ColumnInfo(defaultValue = "0")
private boolean favorite = false;
+
+ @Expose
@Nullable
+ @SerializedName("etag")
private String eTag;
+
@NonNull
@ColumnInfo(defaultValue = "")
private String excerpt = "";
+
@ColumnInfo(defaultValue = "0")
private int scrollY = 0;
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 85e02617..3d0147fb 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
@@ -1,7 +1,9 @@
package it.niedermann.owncloud.notes.persistence.migration;
+import android.appwidget.AppWidgetManager;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
@@ -14,19 +16,18 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Map;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
+import it.niedermann.owncloud.notes.widget.notelist.NoteListWidget;
+import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget;
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) {
+ public Migration_13_14(@NonNull Context context) {
super(13, 14);
this.context = context;
- this.notifyWidgets = notifyWidgets;
}
/**
@@ -86,6 +87,7 @@ public class Migration_13_14 extends Migration {
}
}
editor.apply();
- notifyWidgets.run();
+ context.sendBroadcast(new Intent(context, SingleNoteWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
+ context.sendBroadcast(new Intent(context, NoteListWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
}
}
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 2732151f..48b7195b 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
@@ -1,7 +1,9 @@
package it.niedermann.owncloud.notes.persistence.migration;
+import android.appwidget.AppWidgetManager;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.util.Log;
@@ -15,19 +17,18 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Map;
import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
+import it.niedermann.owncloud.notes.widget.notelist.NoteListWidget;
+import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget;
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) {
+ public Migration_15_16(@NonNull Context context) {
super(15, 16);
this.context = context;
- this.notifyWidgets = notifyWidgets;
}
/**
@@ -104,6 +105,7 @@ public class Migration_15_16 extends Migration {
}
}
editor.apply();
- notifyWidgets.run();
+ context.sendBroadcast(new Intent(context, SingleNoteWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
+ context.sendBroadcast(new Intent(context, NoteListWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
new file mode 100644
index 00000000..c9bf78da
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
@@ -0,0 +1,81 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+import android.graphics.Color;
+import android.util.Log;
+
+import com.bumptech.glide.load.HttpException;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+
+import java.lang.reflect.Type;
+
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
+
+public class CapabilitiesDeserializer implements JsonDeserializer<Capabilities> {
+
+ private static final String TAG = CapabilitiesDeserializer.class.getSimpleName();
+
+ private static final String JSON_OCS = "ocs";
+ private static final String JSON_OCS_META = "meta";
+ private static final String JSON_OCS_META_STATUSCODE = "statuscode";
+ private static final String JSON_OCS_DATA = "data";
+ private static final String JSON_OCS_DATA_CAPABILITIES = "capabilities";
+ private static final String JSON_OCS_DATA_CAPABILITIES_NOTES = "notes";
+ private static final String JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION = "api_version";
+ private static final String JSON_OCS_DATA_CAPABILITIES_THEMING = "theming";
+ private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR = "color";
+ private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text";
+
+ @Override
+ public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ final Capabilities response = new Capabilities();
+ final JsonObject ocs = json.getAsJsonObject().getAsJsonObject(JSON_OCS);
+ if (ocs.has(JSON_OCS_META)) {
+ final JsonObject meta = ocs.getAsJsonObject(JSON_OCS_META);
+ if (meta.has(JSON_OCS_META_STATUSCODE)) {
+ if (meta.get(JSON_OCS_META_STATUSCODE).getAsInt() == HTTP_UNAVAILABLE) {
+ Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode.");
+ throw new JsonParseException(new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE)));
+ }
+ }
+ }
+ if (ocs.has(JSON_OCS_DATA)) {
+ final JsonObject data = ocs.getAsJsonObject(JSON_OCS_DATA);
+ if (data.has(JSON_OCS_DATA_CAPABILITIES)) {
+ final JsonObject capabilities = data.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES);
+ if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) {
+ final JsonObject notes = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_NOTES);
+ if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) {
+ final JsonElement apiVersion = notes.get(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION);
+ response.setApiVersion(apiVersion.isJsonArray() ? apiVersion.toString() : null);
+ }
+ }
+ if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) {
+ final JsonObject theming = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_THEMING);
+ if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) {
+ try {
+ response.setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR).getAsString())));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) {
+ try {
+ response.setTextColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT).getAsString())));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ return response;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java
new file mode 100644
index 00000000..4ab8371e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java
@@ -0,0 +1,142 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+import com.nextcloud.android.sso.api.NextcloudAPI;
+import com.nextcloud.android.sso.api.ParsedResponse;
+
+import java.util.Calendar;
+import java.util.List;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+import it.niedermann.owncloud.notes.shared.model.NotesSettings;
+import retrofit2.Call;
+import retrofit2.NextcloudRetrofitApiBuilder;
+
+/**
+ * Compatibility layer to support multiple API versions
+ */
+public class NotesAPI {
+
+ private static final String TAG = NotesAPI.class.getSimpleName();
+
+ private static final String API_ENDPOINT_NOTES_1_0 = "/index.php/apps/notes/api/v1/";
+ private static final String API_ENDPOINT_NOTES_0_2 = "/index.php/apps/notes/api/v0.2/";
+
+ @NonNull
+ private final ApiVersion usedApiVersion;
+ private final NotesAPI_0_2 notesAPI_0_2;
+ private final NotesAPI_1_0 notesAPI_1_0;
+
+ public NotesAPI(@NonNull NextcloudAPI nextcloudAPI, @Nullable ApiVersion preferredApiVersion) {
+ if (preferredApiVersion == null) {
+ Log.i(TAG, "Using " + ApiVersion.API_VERSION_0_2 + ", preferredApiVersion is null");
+ usedApiVersion = ApiVersion.API_VERSION_0_2;
+ notesAPI_0_2 = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_NOTES_0_2).create(NotesAPI_0_2.class);
+ notesAPI_1_0 = null;
+ } else if (ApiVersion.API_VERSION_1_0.equals(preferredApiVersion)) {
+ Log.i(TAG, "Using " + ApiVersion.API_VERSION_1_0);
+ usedApiVersion = ApiVersion.API_VERSION_1_0;
+ notesAPI_0_2 = null;
+ notesAPI_1_0 = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_NOTES_1_0).create(NotesAPI_1_0.class);
+ } else if (ApiVersion.API_VERSION_0_2.equals(preferredApiVersion)) {
+ Log.i(TAG, "Using " + ApiVersion.API_VERSION_0_2);
+ usedApiVersion = ApiVersion.API_VERSION_0_2;
+ notesAPI_0_2 = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_NOTES_0_2).create(NotesAPI_0_2.class);
+ notesAPI_1_0 = null;
+ } else {
+ Log.w(TAG, "Unsupported API version " + preferredApiVersion + " - try using " + ApiVersion.API_VERSION_0_2);
+ usedApiVersion = ApiVersion.API_VERSION_0_2;
+ notesAPI_0_2 = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_NOTES_0_2).create(NotesAPI_0_2.class);
+ notesAPI_1_0 = null;
+ }
+ }
+
+ public Observable<ParsedResponse<List<Note>>> getNotes(@NonNull Calendar lastModified, String lastETag) {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.getNotes(lastModified.getTimeInMillis() / 1_000, lastETag);
+ } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) {
+ return notesAPI_0_2.getNotes(lastModified.getTimeInMillis() / 1_000, lastETag);
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support getNotes().");
+ }
+ }
+
+ public Call<Note> createNote(Note note) {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.createNote(note);
+ } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) {
+ return notesAPI_0_2.createNote(new Note_0_2(note));
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support createNote().");
+ }
+ }
+
+ public Call<Note> editNote(@NonNull Note note) {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.editNote(note, note.getRemoteId());
+ } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) {
+ return notesAPI_0_2.editNote(new Note_0_2(note), note.getRemoteId());
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support editNote().");
+ }
+ }
+
+ public Call<Void> deleteNote(long noteId) {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.deleteNote(noteId);
+ } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) {
+ return notesAPI_0_2.deleteNote(noteId);
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support createNote().");
+ }
+ }
+
+
+ public Call<NotesSettings> getSettings() {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.getSettings();
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support getSettings().");
+ }
+ }
+
+ public Call<NotesSettings> putSettings(NotesSettings settings) {
+ if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
+ return notesAPI_1_0.putSettings(settings);
+ } else {
+ throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support putSettings().");
+ }
+ }
+
+ /**
+ * {@link ApiVersion#API_VERSION_0_2} didn't have a separate <code>title</code> property.
+ */
+ static class Note_0_2 {
+ @Expose
+ public final String category;
+ @Expose
+ public final Calendar modified;
+ @Expose
+ public final String content;
+ @Expose
+ public final boolean favorite;
+
+ private Note_0_2(Note note) {
+ if (note == null) {
+ throw new IllegalArgumentException(Note.class.getSimpleName() + " can not be converted to " + Note_0_2.class.getSimpleName() + " because it is null.");
+ }
+ this.category = note.getCategory();
+ this.modified = note.getModified();
+ this.content = note.getContent();
+ this.favorite = note.getFavorite();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java
new file mode 100644
index 00000000..fd642064
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java
@@ -0,0 +1,36 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+
+import java.util.List;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+/**
+ * @link <a href="https://github.com/nextcloud/notes/wiki/API-0.2">Notes API v0.2</a>
+ */
+public interface NotesAPI_0_2 {
+
+ @GET("notes")
+ Observable<ParsedResponse<List<Note>>> getNotes(@Query("pruneBefore") long lastModified, @Header("If-None-Match") String lastETag);
+
+ @POST("notes")
+ Call<Note> createNote(@Body NotesAPI.Note_0_2 note);
+
+ @PUT("notes/{remoteId}")
+ Call<Note> editNote(@Body NotesAPI.Note_0_2 note, @Path("remoteId") long remoteId);
+
+ @DELETE("notes/{remoteId}")
+ Call<Void> deleteNote(@Path("remoteId") long noteId);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java
new file mode 100644
index 00000000..dfc176c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_1_0.java
@@ -0,0 +1,43 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+
+import java.util.List;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.NotesSettings;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+/**
+ * @link <a href="https://github.com/nextcloud/notes/blob/master/docs/api/README.md">Notes API v1</a>
+ */
+public interface NotesAPI_1_0 {
+
+ @GET("notes")
+ Observable<ParsedResponse<List<Note>>> getNotes(@Query("pruneBefore") long lastModified, @Header("If-None-Match") String lastETag);
+
+ @POST("notes")
+ Call<Note> createNote(@Body Note note);
+
+ @PUT("notes/{remoteId}")
+ Call<Note> editNote(@Body Note note, @Path("remoteId") long remoteId);
+
+ @DELETE("notes/{remoteId}")
+ Call<Void> deleteNote(@Path("remoteId") long noteId);
+
+ @GET("settings")
+ Call<NotesSettings> getSettings();
+
+ @PUT("settings")
+ Call<NotesSettings> putSettings(@Body NotesSettings settings);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java
new file mode 100644
index 00000000..27ef57c4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java
@@ -0,0 +1,18 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+
+/**
+ * @link <a href="https://deck.readthedocs.io/en/latest/API/">Deck REST API</a>
+ */
+public interface OcsAPI {
+
+ @GET("capabilities?format=json")
+ Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java
index b4c62f5b..ee2fdc3a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java
@@ -3,6 +3,7 @@ package it.niedermann.owncloud.notes.shared.model;
import androidx.annotation.NonNull;
+import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,6 +12,14 @@ public class ApiVersion implements Comparable<ApiVersion> {
private static final Pattern NUMBER_EXTRACTION_PATTERN = Pattern.compile("[0-9]+");
private static final ApiVersion VERSION_1_2 = new ApiVersion("1.2", 1, 2);
+ public static final ApiVersion API_VERSION_0_2 = new ApiVersion(0, 2);
+ public static final ApiVersion API_VERSION_1_0 = new ApiVersion(1, 0);
+
+ public static final ApiVersion[] SUPPORTED_API_VERSIONS = new ApiVersion[]{
+ API_VERSION_1_0,
+ API_VERSION_0_2
+ };
+
private String originalVersion = "?";
private final int major;
private final int minor;
@@ -66,7 +75,7 @@ public class ApiVersion implements Comparable<ApiVersion> {
* 1 if the compared major version is <strong>lower</strong> than the current major version
*/
@Override
- public int compareTo(ApiVersion compare) {
+ public int compareTo(@NonNull ApiVersion compare) {
if (compare.getMajor() > getMajor()) {
return -1;
} else if (compare.getMajor() < getMajor()) {
@@ -77,7 +86,21 @@ public class ApiVersion implements Comparable<ApiVersion> {
public boolean supportsSettings() {
// TODO
- return true;//getMajor() >= VERSION_1_2.getMajor() && getMinor() >= VERSION_1_2.getMinor();
+ return true;
+// return getMajor() >= 1 && getMinor() >= 2;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ApiVersion that = (ApiVersion) o;
+ return compareTo(that) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(major, minor);
}
@NonNull
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 1c1bed3d..5514a91b 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
@@ -6,6 +6,7 @@ import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.load.HttpException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
@@ -38,12 +39,17 @@ public class Capabilities {
private String apiVersion = null;
@ColorInt
- private Integer color = -16743735;
+ private int color = -16743735;
@ColorInt
- private Integer textColor = -16777216;
+ private int textColor = -16777216;
@Nullable
- private final String eTag;
+ private String eTag;
+ public Capabilities() {
+
+ }
+
+ @VisibleForTesting
public Capabilities(@NonNull String response, @Nullable String eTag) throws NextcloudHttpRequestFailedException {
this.eTag = eTag;
final JSONObject ocs;
@@ -92,6 +98,10 @@ public class Capabilities {
}
}
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
public String getApiVersion() {
return apiVersion;
}
@@ -101,14 +111,26 @@ public class Capabilities {
return eTag;
}
- public Integer getColor() {
+ public void setETag(@Nullable String eTag) {
+ this.eTag = eTag;
+ }
+
+ public int getColor() {
return color;
}
- public Integer getTextColor() {
+ 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() {
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
index 2c329727..707931b0 100644
--- 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
@@ -2,8 +2,8 @@ package it.niedermann.owncloud.notes.shared.model;
import androidx.annotation.NonNull;
-public interface IResponseCallback {
- void onSuccess();
+public interface IResponseCallback<T> {
+ void onSuccess(T result);
void onError(@NonNull Throwable t);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NotesSettings.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NotesSettings.java
new file mode 100644
index 00000000..ff11434f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NotesSettings.java
@@ -0,0 +1,34 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import androidx.annotation.Nullable;
+
+public class NotesSettings {
+
+ @Nullable
+ private String notesPath;
+ @Nullable
+ private String fileSuffix;
+
+ public NotesSettings(@Nullable String notesPath, @Nullable String fileSuffix) {
+ this.notesPath = notesPath;
+ this.fileSuffix = fileSuffix;
+ }
+
+ @Nullable
+ public String getNotesPath() {
+ return notesPath;
+ }
+
+ public void setNotesPath(@Nullable String notesPath) {
+ this.notesPath = notesPath;
+ }
+
+ @Nullable
+ public String getFileSuffix() {
+ return fileSuffix;
+ }
+
+ public void setFileSuffix(@Nullable String fileSuffix) {
+ this.fileSuffix = fileSuffix;
+ }
+}
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
deleted file mode 100644
index dca53fb9..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import androidx.annotation.Nullable;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-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}).
- */
-public class ServerResponse {
-
- public static class NoteResponse extends ServerResponse {
- public NoteResponse(NotesClient.ResponseData response) {
- super(response);
- }
-
- public Note getNote() throws JSONException {
- return getNoteFromJSON(new JSONObject(getContent()));
- }
- }
-
- public static class NotesResponse extends ServerResponse {
- public NotesResponse(NotesClient.ResponseData response) {
- super(response);
- }
-
- 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);
- notesList.add(getNoteFromJSON(json));
- }
- return notesList;
- }
- }
-
-
- private final NotesClient.ResponseData response;
-
- ServerResponse(NotesClient.ResponseData response) {
- this.response = response;
- }
-
- protected String getContent() {
- return response == null ? null : response.getContent();
- }
-
- public String getETag() {
- return response.getETag();
- }
-
- public Calendar getLastModified() {
- return response.getLastModified();
- }
-
- @Nullable
- public String getSupportedApiVersions() {
- return response.getSupportedApiVersions();
- }
-
- Note getNoteFromJSON(JSONObject json) throws JSONException {
- long remoteId = 0;
- String title = "";
- String content = "";
- Calendar modified = null;
- boolean favorite = false;
- String category = "";
- String etag = null;
- if (!json.isNull(NotesClient.JSON_ID)) {
- remoteId = json.getLong(NotesClient.JSON_ID);
- }
- if (!json.isNull(NotesClient.JSON_TITLE)) {
- title = json.getString(NotesClient.JSON_TITLE);
- }
- if (!json.isNull(NotesClient.JSON_CONTENT)) {
- content = json.getString(NotesClient.JSON_CONTENT);
- }
- if (!json.isNull(NotesClient.JSON_MODIFIED)) {
- modified = Calendar.getInstance();
- modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1_000);
- }
- if (!json.isNull(NotesClient.JSON_FAVORITE)) {
- favorite = json.getBoolean(NotesClient.JSON_FAVORITE);
- }
- if (!json.isNull(NotesClient.JSON_CATEGORY)) {
- category = json.getString(NotesClient.JSON_CATEGORY);
- }
- if (!json.isNull(NotesClient.JSON_ETAG)) {
- etag = json.getString(NotesClient.JSON_ETAG);
- }
- return new Note(remoteId, modified, title, content, category, favorite, etag);
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerSettings.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerSettings.java
deleted file mode 100644
index e977b697..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerSettings.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.Serializable;
-
-import static it.niedermann.owncloud.notes.persistence.NotesClient.JSON_SETTINGS_FILE_SUFFIX;
-import static it.niedermann.owncloud.notes.persistence.NotesClient.JSON_SETTINGS_NOTES_PATH;
-
-public class ServerSettings implements Serializable {
- private String notesPath = "";
- private String fileSuffix = "";
-
- public ServerSettings(String notesPath, String fileSuffix) {
- setNotesPath(notesPath);
- setFileSuffix(fileSuffix);
- }
-
- public static ServerSettings from(JSONObject settings) throws JSONException {
- String notesPath = "";
- if (settings.has(JSON_SETTINGS_NOTES_PATH)) {
- notesPath = settings.getString(JSON_SETTINGS_NOTES_PATH);
- }
- String fileSuffix = "";
- if (settings.has(JSON_SETTINGS_FILE_SUFFIX)) {
- fileSuffix = settings.getString(JSON_SETTINGS_FILE_SUFFIX);
- }
- return new ServerSettings(notesPath, fileSuffix);
- }
-
- public String getNotesPath() {
- return notesPath;
- }
-
- public void setNotesPath(String notesPath) {
- this.notesPath = notesPath;
- }
-
- public String getFileSuffix() {
- return fileSuffix;
- }
-
- public void setFileSuffix(String fileSuffix) {
- this.fileSuffix = fileSuffix;
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java
index 2031568b..41ba850a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java
@@ -3,4 +3,11 @@ package it.niedermann.owncloud.notes.shared.model;
public class SyncResultStatus {
public boolean pullSuccessful = true;
public boolean pushSuccessful = true;
+
+ public static final SyncResultStatus FAILED = new SyncResultStatus();
+
+ static {
+ FAILED.pullSuccessful = false;
+ FAILED.pushSuccessful = false;
+ }
}
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
index ad3830a4..9ec3e355 100644
--- 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
@@ -14,7 +14,7 @@ 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 it.niedermann.owncloud.notes.persistence.NotesRepository;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.lifecycle.Transformations.map;
@@ -28,20 +28,20 @@ public class NoteListViewModel extends AndroidViewModel {
private static final String TAG = NoteListViewModel.class.getSimpleName();
@NonNull
- private final NotesDatabase db;
+ private final NotesRepository repo;
public NoteListViewModel(@NonNull Application application) {
super(application);
- this.db = NotesDatabase.getInstance(application);
+ this.repo = NotesRepository.getInstance(application);
}
public LiveData<List<NavigationItem>> getAdapterCategories(Long accountId) {
return distinctUntilChanged(
- switchMap(distinctUntilChanged(db.getNoteDao().count$(accountId)), (count) -> {
+ switchMap(distinctUntilChanged(repo.count$(accountId)), (count) -> {
Log.v(TAG, "[getAdapterCategories] countLiveData: " + count);
- return switchMap(distinctUntilChanged(db.getNoteDao().countFavorites$(accountId)), (favoritesCount) -> {
+ return switchMap(distinctUntilChanged(repo.countFavorites$(accountId)), (favoritesCount) -> {
Log.v(TAG, "[getAdapterCategories] getFavoritesCountLiveData: " + favoritesCount);
- return map(distinctUntilChanged(db.getNoteDao().getCategories$(accountId)), fromDatabase -> {
+ return map(distinctUntilChanged(repo.getCategories$(accountId)), fromDatabase -> {
final List<NavigationItem.CategoryNavigationItem> categories = convertToCategoryNavigationItem(getApplication(), fromDatabase);
final List<NavigationItem> items = new ArrayList<>(fromDatabase.size() + 3);
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 98ebf2fe..7cc84de3 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,20 +13,20 @@ import android.widget.RemoteViews;
import java.util.NoSuchElementException;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
public class NoteListWidget extends AppWidgetProvider {
private static final String TAG = NoteListWidget.class.getSimpleName();
static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
- final NotesDatabase db = NotesDatabase.getInstance(context);
+ final NotesRepository repo = NotesRepository.getInstance(context);
RemoteViews views;
for (int appWidgetId : appWidgetIds) {
try {
- final NotesListWidgetData data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId);
+ final NotesListWidgetData data = repo.getNoteListWidgetData(appWidgetId);
final Intent serviceIntent = new Intent(context, NoteListWidgetService.class);
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
@@ -80,10 +80,10 @@ public class NoteListWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
- final NotesDatabase db = NotesDatabase.getInstance(context);
+ final NotesRepository repo = NotesRepository.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- new Thread(() -> db.getWidgetNotesListDao().removeNoteListWidget(appWidgetId)).start();
+ new Thread(() -> repo.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 2c2aa086..a750ee1e 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
@@ -21,11 +21,9 @@ import it.niedermann.owncloud.notes.databinding.ActivityNoteListConfigurationBin
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.NotesRepository;
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;
@@ -43,14 +41,14 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
private ActivityNoteListConfigurationBinding binding;
private NoteListViewModel viewModel;
private NavigationAdapter adapterCategories;
- private NotesDatabase db = null;
+ private NotesRepository repo = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
- db = NotesDatabase.getInstance(this);
+ repo = NotesRepository.getInstance(this);
final Bundle extras = getIntent().getExtras();
if (extras != null) {
@@ -107,7 +105,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId());
new Thread(() -> {
- db.getWidgetNotesListDao().createOrUpdateNoteListWidgetData(data);
+ repo.createOrUpdateNoteListWidgetData(data);
final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), NoteListWidget.class)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
@@ -126,7 +124,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
new Thread(() -> {
try {
- this.localAccount = db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name);
+ this.localAccount = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show();
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 7917b25a..b7482233 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
@@ -19,7 +19,7 @@ import java.util.List;
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.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
@@ -37,7 +37,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
private final Context context;
private final int appWidgetId;
- private final NotesDatabase db;
+ private final NotesRepository repo;
@NonNull
private final List<Note> dbNotes = new ArrayList<>();
private NotesListWidgetData data;
@@ -45,7 +45,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
NoteListWidgetFactory(Context context, Intent intent) {
this.context = context;
this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- db = NotesDatabase.getInstance(context);
+ repo = NotesRepository.getInstance(context);
}
@Override
@@ -57,21 +57,21 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
public void onDataSetChanged() {
dbNotes.clear();
try {
- data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId);
+ data = repo.getNoteListWidgetData(appWidgetId);
Log.v(TAG, "--- data - " + data);
switch (data.getMode()) {
case MODE_DISPLAY_ALL:
- dbNotes.addAll(db.getNoteDao().searchRecentByModified(data.getAccountId(), "%"));
+ dbNotes.addAll(repo.searchRecentByModified(data.getAccountId(), "%"));
break;
case MODE_DISPLAY_STARRED:
- dbNotes.addAll(db.getNoteDao().searchFavoritesByModified(data.getAccountId(), "%"));
+ dbNotes.addAll(repo.searchFavoritesByModified(data.getAccountId(), "%"));
break;
case MODE_DISPLAY_CATEGORY:
default:
if (data.getCategory() != null) {
- dbNotes.addAll(db.getNoteDao().searchCategoryByModified(data.getAccountId(), "%", data.getCategory()));
+ dbNotes.addAll(repo.searchCategoryByModified(data.getAccountId(), "%", data.getCategory()));
} else {
- dbNotes.addAll(db.getNoteDao().searchUncategorizedByModified(data.getAccountId(), "%"));
+ dbNotes.addAll(repo.searchUncategorizedByModified(data.getAccountId(), "%"));
}
break;
}
@@ -95,7 +95,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact
final RemoteViews note_content;
if (position == 0) {
- final Account localAccount = db.getAccountDao().getAccountById(data.getAccountId());
+ final Account localAccount = repo.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();
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 3ad56c76..e208603a 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
@@ -11,9 +11,9 @@ import android.util.Log;
import android.widget.RemoteViews;
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.edit.EditNoteActivity;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
public class SingleNoteWidget extends AppWidgetProvider {
@@ -22,11 +22,11 @@ public class SingleNoteWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
final Intent templateIntent = new Intent(context, EditNoteActivity.class);
- final NotesDatabase db = NotesDatabase.getInstance(context);
+ final NotesRepository repo = NotesRepository.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId);
- if(data != null) {
+ final SingleNoteWidgetData data = repo.getSingleNoteWidgetData(appWidgetId);
+ if (data != null) {
templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, data.getAccountId());
final PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent,
@@ -66,10 +66,10 @@ public class SingleNoteWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
- final NotesDatabase db = NotesDatabase.getInstance(context);
+ final NotesRepository repo = NotesRepository.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- new Thread(() -> db.getWidgetSingleNoteDao().removeSingleNoteWidget(appWidgetId)).start();
+ new Thread(() -> repo.removeSingleNoteWidget(appWidgetId)).start();
}
super.onDeleted(context, appWidgetIds);
}
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 db26da24..f6ca4a24 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
@@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
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.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
@@ -22,7 +22,7 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa
private final Context context;
private final int appWidgetId;
- private final NotesDatabase db;
+ private final NotesRepository repo;
@Nullable
private Note note;
@@ -31,7 +31,7 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa
SingleNoteWidgetFactory(Context context, Intent intent) {
this.context = context;
this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- this.db = NotesDatabase.getInstance(context);
+ this.repo = NotesRepository.getInstance(context);
}
@Override
@@ -41,11 +41,11 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa
@Override
public void onDataSetChanged() {
- final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId);
- if(data != null) {
+ final SingleNoteWidgetData data = repo.getSingleNoteWidgetData(appWidgetId);
+ if (data != null) {
final long noteId = data.getNoteId();
Log.v(TAG, "Fetch note with id " + noteId);
- note = db.getNoteDao().getNoteById(noteId);
+ note = repo.getNoteById(noteId);
if (note == null) {
Log.e(TAG, "Error: note not found");
diff --git a/app/src/main/res/layout/activity_import_account.xml b/app/src/main/res/layout/activity_import_account.xml
index 4ad83f02..ed072796 100644
--- a/app/src/main/res/layout/activity_import_account.xml
+++ b/app/src/main/res/layout/activity_import_account.xml
@@ -50,6 +50,19 @@
android:text="@string/choose_account"
app:backgroundTint="@color/defaultBrand" />
+ <TextView
+ android:id="@+id/status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/add_button"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="32dp"
+ android:gravity="center_horizontal"
+ android:textAlignment="center"
+ android:textColor="@color/fg_secondary"
+ android:textSize="18sp"
+ tools:text="@string/you_have_to_be_connected_to_the_internet_in_order_to_add_an_account" />
+
<ProgressBar
android:id="@+id/progress_circular"
android:layout_width="wrap_content"
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index e7050e2e..ea8943dd 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Totes les notes</string>
<string name="label_favorites">Preferides</string>
<string name="action_create">Nota nova</string>
+ <string name="welcome_text">Us donem la benvinguda a %1$s</string>
<string name="action_settings">Paràmetres</string>
<string name="action_trashbin">Notes suprimides</string>
<string name="action_search">Cerca</string>
<string name="action_sorting_method">Mètode d\'ordenació</string>
<string name="simple_cancel">Cancel·la</string>
<string name="simple_edit">Edita</string>
+ <string name="simple_remove">Suprimeix</string>
<string name="action_edit_save">Desa</string>
<string name="simple_about">Quant a</string>
<string name="simple_link">Enllaç</string>
@@ -42,7 +44,11 @@
<string name="settings_font_title">Tipus de lletra de mida fixa</string>
<string name="settings_font_size">Mida de la lletra</string>
<string name="settings_wifi_only">Sincronitza només per la connexió sense fil</string>
+ <string name="settings_lock">Blocatge de l\'aplicació (beta)</string>
+ <string name="settings_lock_summary">Credencials del dispositiu</string>
<string name="settings_background_sync">Sincronització en segon pla</string>
+ <string name="settings_prevent_screen_capture">Impedeix la captura de la pantalla</string>
+
<string name="error_sync">S\'ha produït un error en la sincronització: %1$s</string>
<string name="error_synchronization">S\'ha produït un error en la sincronització</string>
<string name="error_no_network">No hi ha connexió a la xarxa</string>
@@ -111,6 +117,7 @@
<string name="error_dialog_title">Oh, no! I ara què? </string>
<string name="error_dialog_tip_token_mismatch_retry">Proveu de forçar el tancament de l\'aplicació i reinicieu-la. Pot ser que hi hagi hagut una connexió incorrecta a l\'aplicació Nextcloud.</string>
<string name="error_dialog_tip_token_mismatch_clear_storage">Si el problema persisteix, proveu d\'esborrar l\'emmagatzematge de totes dues aplicacions: Nextcloud i Notes del Nextcloud per tal de resoldre el problema.</string>
+ <string name="error_dialog_tip_clear_storage">Per a esborrar l\'emmagatzematge, obriu la informació de l\'aplicació i seleccioneu Emmagatzematge → Esborra l\'emmagatzematge. ⚠️ Avís: això també esborrarà les notes que encara no s\'hagin sincronitzat!</string>
<string name="error_dialog_tip_files_outdated">Sembla que la vostra aplicació Nextcloud està obsoleta. Visiteu la Play Store o l\'F-Droid per obtenir la darrera versió.</string>
<string name="error_dialog_tip_files_force_stop">Sembla que hi ha algun problema amb la vostra aplicació Nextcloud. Intenteu forçar l\'aturada tant de l\'aplicació Nextcloud com de l\'aplicació Notes del Nextcloud.</string>
<string name="error_dialog_tip_files_delete_storage">Si forçar-ne l\'aturada no ajuda, podeu provar d\'esborrar l\'emmagatzematge de totes dues aplicacions.</string>
@@ -131,7 +138,9 @@
<string name="change_note_title">Canvia el títol de la nota</string>
<string name="menu_edit_title">Edita el títol</string>
<string name="settings_branding">Marca</string>
+ <string name="settings_gridview">Visualització de quadrícula</string>
<string name="simple_security">Seguretat</string>
+ <string name="appearance_and_behavior">Aparença i comportament</string>
<string name="simple_synchronization">Sincronització</string>
<string name="simple_behavior">Comportament</string>
<string name="manage_accounts">Gestiona els comptes</string>
@@ -238,5 +247,13 @@
<string name="error_action_open_network">Paràmetres de la xarxa</string>
<string name="no_account_configured_yet">Encara no s\'ha configurat cap compte</string>
<string name="no_other_accounts">Encara no heu configurat cap altre compte.</string>
+ <string name="choose_account">Trieu un compte</string>
<string name="context_based_formatting">Element emergent de format basat en el context</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Si suprimiu el compte %1$s, també se suprimirà un canvi sense sincronitzar de manera irrecuperable.</item>
+ <item quantity="other">Si suprimiu el compte %1$s, també se suprimiran %2$d canvis sense sincronitzar de manera irrecuperable.</item>
+ </plurals>
+ <string name="remove_account">Suprimeix %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Heu de tenir connexió a Internet per a afegir un compte.</string>
</resources>
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index 6dc44ea6..a5252866 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Všechny poznámky</string>
<string name="label_favorites">Oblíbené</string>
<string name="action_create">Nová poznámka</string>
+ <string name="welcome_text">Vítejte v %1$s</string>
<string name="action_settings">Nastavení</string>
<string name="action_trashbin">Smazané poznámky</string>
<string name="action_search">Hledat</string>
<string name="action_sorting_method">Způsob řazení</string>
<string name="simple_cancel">Storno</string>
<string name="simple_edit">Upravit</string>
+ <string name="simple_remove">Odebrat</string>
<string name="action_edit_save">Uložit</string>
<string name="simple_about">O aplikaci</string>
<string name="simple_link">Odkaz</string>
@@ -253,5 +255,15 @@
<string name="error_action_open_network">Nastavení sítě</string>
<string name="no_account_configured_yet">Zatím není nastavený žádný účet</string>
<string name="no_other_accounts">Zatím jste nenastavili žádné další účty.</string>
+ <string name="choose_account">Zvolte účet</string>
<string name="context_based_formatting">Kontextové vyskakovací okno formátování</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Odebrání účtu %1$s také nenahraditelně odstraní jednu nesynchronizovanou změnu.</item>
+ <item quantity="few">Odebrání účtu %1$s také nenahraditelně odstraní %2$d nesynchronizovaných změn.</item>
+ <item quantity="many">Odebrání účtu %1$s také nenahraditelně odstraní %2$d nesynchronizovaných změn.</item>
+ <item quantity="other">Odebrání účtu %1$s také nenahraditelně odstraní %2$d nesynchronizované změny.</item>
+ </plurals>
+ <string name="remove_account">Odebrat %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Abyste mohli přidat účet je třeba, abyste byli připojení k Internetu.</string>
</resources>
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 094f3e18..c74c33cc 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -193,4 +193,4 @@
<string name="error_action_open_battery_settings">Batteriindstillinger</string>
<string name="error_action_open_deck_info">Åbn App info</string>
<string name="error_action_open_network">Netværksindstillinger</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 9f7e63bd..a90c81e1 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Alle Notizen</string>
<string name="label_favorites">Favoriten</string>
<string name="action_create">Neue Notiz</string>
+ <string name="welcome_text">Willkommen bei %1$s</string>
<string name="action_settings">Einstellungen</string>
<string name="action_trashbin">Gelöschte Notizen</string>
<string name="action_search">Suche</string>
<string name="action_sorting_method">Sortierverfahren</string>
<string name="simple_cancel">Abbrechen</string>
<string name="simple_edit">Bearbeiten</string>
+ <string name="simple_remove">Entfernen</string>
<string name="action_edit_save">Speichern</string>
<string name="simple_about">Über</string>
<string name="simple_link">Link</string>
@@ -245,5 +247,13 @@
<string name="error_action_open_network">Netzwerkeinstellungen</string>
<string name="no_account_configured_yet">Bislang kein Konto eingerichtet</string>
<string name="no_other_accounts">Sie haben bislang keine weiteren Konten eingerichtet.</string>
+ <string name="choose_account">Konto auswählen</string>
<string name="context_based_formatting">Popup für die kontextbasierte Formatierung</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Beim Entfernen des Kontos %1$s wird auch eine unsynchronisierte Änderung unwiderruflich gelöscht werden.</item>
+ <item quantity="other">Beim Entfernen des Kontos %1$s werden auch %2$d unsynchronisierte Änderungen unwiderruflich gelöscht werden.</item>
+ </plurals>
+ <string name="remove_account">%1$s entfernen</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Sie müssen mit dem Internet verbunden sein, um ein Konto hinzufügen zu können.</string>
</resources>
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 67236078..10afe072 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -239,4 +239,4 @@
<string name="no_account_configured_yet">Δεν έχει ρυθμιστεί ακόμη κάποιος λογαριασμός</string>
<string name="no_other_accounts">Δεν έχετε ρυθμίσει ακόμη άλλους λογαριασμούς.</string>
<string name="context_based_formatting">Μορφοποίηση βάσει περιεχομένου popover</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 326d965e..4edaecf3 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Todas las notas</string>
<string name="label_favorites">Favoritos</string>
<string name="action_create">Nueva nota</string>
+ <string name="welcome_text">Bienvenido a %1$s</string>
<string name="action_settings">Ajustes</string>
<string name="action_trashbin">Notas eliminadas</string>
<string name="action_search">Buscar</string>
<string name="action_sorting_method">Método de orden</string>
<string name="simple_cancel">Cancelar</string>
<string name="simple_edit">Editar</string>
+ <string name="simple_remove">Borrar</string>
<string name="action_edit_save">Guardar</string>
<string name="simple_about">Acerca de</string>
<string name="simple_link">Enlace</string>
@@ -42,7 +44,11 @@
<string name="settings_font_title">Tipo de letra monoespaciado</string>
<string name="settings_font_size">Tamaño de fuente</string>
<string name="settings_wifi_only">Sincronizar solo sobre Wi-Fi</string>
+ <string name="settings_lock">Bloqueo de App (Beta)</string>
+ <string name="settings_lock_summary">Credenciales del dispositivo</string>
<string name="settings_background_sync">Sincronización en segundo plano</string>
+ <string name="settings_prevent_screen_capture">Evitar la captura de pantalla</string>
+
<string name="error_sync">Fallo en la sincronización: %1$s</string>
<string name="error_synchronization">Fallo en la sincronización</string>
<string name="error_no_network">Sin conexión de red</string>
@@ -132,7 +138,9 @@
<string name="change_note_title">Cambiar título de la nota</string>
<string name="menu_edit_title">Editar título</string>
<string name="settings_branding">Marca</string>
+ <string name="settings_gridview">Vista en cuadrícula</string>
<string name="simple_security">Seguridad</string>
+ <string name="appearance_and_behavior">Apariencia y comportamiento</string>
<string name="simple_synchronization">Sincronización</string>
<string name="simple_behavior">Comportamiento</string>
<string name="manage_accounts">Gestionar cuentas</string>
@@ -239,5 +247,13 @@
<string name="error_action_open_network">Configuración de red</string>
<string name="no_account_configured_yet">Todavía no hay una cuenta configurada</string>
<string name="no_other_accounts">Aún no ha configurado ninguna otra cuenta.</string>
+ <string name="choose_account">Elija una cuenta</string>
<string name="context_based_formatting">Ventana emergente para formato basado en el contexto</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">La eliminación de la cuenta %1$s también eliminará irremediablemente un cambio no sincronizado. </item>
+ <item quantity="other">La eliminación de la cuenta %1$s también eliminará irremediablemente %2$d cambios no sincronizados.</item>
+ </plurals>
+ <string name="remove_account">Eliminar %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Tienes que estar conectado a internet para poder añadir una cuenta.</string>
</resources>
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 0163ab3d..c7d6a119 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Ohar guztiak</string>
<string name="label_favorites">Gogokoak</string>
<string name="action_create">Ohar berria</string>
+ <string name="welcome_text">Ongi etorri %1$s(e)ra</string>
<string name="action_settings">Ezarpenak</string>
<string name="action_trashbin">Ezabatutako oharrak</string>
<string name="action_search">Bilatu</string>
<string name="action_sorting_method">Ordenatze metodoa</string>
<string name="simple_cancel">Utzi</string>
<string name="simple_edit">Editatu</string>
+ <string name="simple_remove">Kendu</string>
<string name="action_edit_save">Gorde</string>
<string name="simple_about">Honi buruz</string>
<string name="simple_link">Esteka</string>
@@ -45,6 +47,8 @@
<string name="settings_lock">Aplikazioen blokeoa (Beta)</string>
<string name="settings_lock_summary">Gailuaren kredentzialak</string>
<string name="settings_background_sync">Atzeko planoko sinkronizazioa</string>
+ <string name="settings_prevent_screen_capture">Saihestu pantaila argazkia</string>
+
<string name="error_sync">Sinkronizazioak huts egin du: %1$s</string>
<string name="error_synchronization">Sinkronizazioak huts egin du</string>
<string name="error_no_network">Ez dago sare konexiorik</string>
@@ -136,6 +140,7 @@
<string name="settings_branding">Marka</string>
<string name="settings_gridview">Sareta ikuspegia</string>
<string name="simple_security">Segurtasuna</string>
+ <string name="appearance_and_behavior">Itxura eta portaera</string>
<string name="simple_synchronization">Sinkronizazioa</string>
<string name="simple_behavior">Portaera</string>
<string name="manage_accounts">Kudeatu kontuak</string>
@@ -242,5 +247,13 @@
<string name="error_action_open_network">Sareko ezarpenak</string>
<string name="no_account_configured_yet">Oraindik ez da konturik konfiguratu </string>
<string name="no_other_accounts">Oraindik ez duzu konturik konfiguratu.</string>
+ <string name="choose_account">Aukeratu kontua</string>
<string name="context_based_formatting">Testuinguruan oinarritutako formateatze leiho gainerakorra </string>
+ <plurals name="remove_account_message">
+ <item quantity="one"> %1$skontua kentzeak berreskuraezinak diren sinkronizatu gabeko aldaketak ere ezabatuko ditu. </item>
+ <item quantity="other">%1$s kontua kentzeak berreskuraezinak diren%2$d sinkronizatu gabeko aldaketak ere ezabatuko ditu. </item>
+ </plurals>
+ <string name="remove_account">%1$s kendu </string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Kontu bat gehitzeko internetera konektatuta egon behar zara.</string>
</resources>
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 4dff5da4..c964dbf0 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -214,4 +214,4 @@
<string name="error_action_open_network">تنظیمات شبکه</string>
<string name="no_other_accounts">شما تاکنون هیچ حساب کاربری دیگری را ایجاد نکرده‌اید.</string>
<string name="context_based_formatting">بازپخش قالب بندی مبتنی بر متن</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index f20f4b78..6dec8b43 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -240,4 +240,4 @@
<string name="no_account_configured_yet">Aucun compte n\'est encore configuré</string>
<string name="no_other_accounts">Vous n\'avez pas encore configuré d\'autres comptes.</string>
<string name="context_based_formatting">Suggestions de mise en forme contextuelle</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index b0a6fbba..8f1b72df 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -45,6 +45,8 @@
<string name="settings_lock">Bloqueo de aplis (beta)</string>
<string name="settings_lock_summary">Credenciais do dispositivo</string>
<string name="settings_background_sync">Sincronización do traballo en segundo plano</string>
+ <string name="settings_prevent_screen_capture">Evitar a captura da pantalla</string>
+
<string name="error_sync">Produciuse un fallo na sincronización: %1$s</string>
<string name="error_synchronization">Produciuse un fallo na sincronización</string>
<string name="error_no_network">Sen conexión de rede</string>
@@ -136,6 +138,7 @@
<string name="settings_branding">Xestión da marca</string>
<string name="settings_gridview">Ver como grella</string>
<string name="simple_security">Seguridade</string>
+ <string name="appearance_and_behavior">Aparencia e comportamento</string>
<string name="simple_synchronization">Sincronización</string>
<string name="simple_behavior">Comportamento</string>
<string name="manage_accounts">Xestionar contas</string>
@@ -243,4 +246,4 @@
<string name="no_account_configured_yet">Aínda non hai ningunha conta configurada</string>
<string name="no_other_accounts">Aínda non ten configurada ningunha outra conta.</string>
<string name="context_based_formatting">Xanela emerxente de formato baseado no contexto</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 1a90ec87..53ac6e35 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -189,4 +189,4 @@
<string name="simple_other">אחר</string>
<string name="error_action_open_deck_info">פתיחת פרטי יישומון</string>
<string name="error_action_open_network">הגדרות רשת</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 013b2929..51018133 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -247,4 +247,4 @@
<string name="no_account_configured_yet">Još uvijek nema konfiguriranih računa</string>
<string name="no_other_accounts">Nemate konfiguriranih računa.</string>
<string name="context_based_formatting">Skočni okvir za oblikovanje ovisno o kontekstu</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index b78dda02..41bfe6c1 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -45,6 +45,8 @@
<string name="settings_lock">Alkalmazás zár (Béta)</string>
<string name="settings_lock_summary">Eszközhitelesítő-adatok</string>
<string name="settings_background_sync">Háttér-szinkronizálás</string>
+ <string name="settings_prevent_screen_capture">A képernyő rögzítésének megakadályozása</string>
+
<string name="error_sync">Szinkronizálás sikertelen: %1$s</string>
<string name="error_synchronization">Szinkronizálás sikertelen</string>
<string name="error_no_network">Nincs hálózati kapcsolat</string>
@@ -136,6 +138,7 @@
<string name="settings_branding">Márkázas</string>
<string name="settings_gridview">Rács nézet</string>
<string name="simple_security">Biztonság</string>
+ <string name="appearance_and_behavior">Megjelenés és viselkedés</string>
<string name="simple_synchronization">Szinkronizálás</string>
<string name="simple_behavior">Viselkedés</string>
<string name="manage_accounts">Fiókok kezelése</string>
@@ -194,7 +197,7 @@
<string name="formatting_help_cbf_body_2">Csak válasszon ki egy szövegtartományt, vagy érintse meg a kurzort bármely helyen, és megjelenik egy előugró menü, amely az alapértelmezett bejegyzések mellett %1$s, %2$s, %3$s tartalmaz olyan bejegyzéseket, mint %4$s vagy %5$s.</string>
<string name="formatting_help_text_title">Szöveg</string>
- <string name="formatting_help_text_body">Nagyon egyszerűen írhat Markdownnal %1$sfélkövéren,%1$s valamint %2$sdőlten.%2$s Át is %3$shúzhat%3$s szavakat, valamint [linkelhet a Nextcloudra](https://nextcloud.com).</string>
+ <string name="formatting_help_text_body">Nagyon egyszerűen írhat Markdownnal %1$sfélkövéren%1$s, valamint %2$sdőlten%2$s. Át is %3$shúzhat%3$s szavakat, valamint [hivatkozhat a Nextcloudra](https://nextcloud.com).</string>
<string name="formatting_help_lists_title">Listák</string>
<string name="formatting_help_lists_body_1">Néha számozott listákat szeretne:</string>
@@ -213,17 +216,17 @@
<string name="formatting_help_checkboxes_body_3">2. elem</string>
<string name="formatting_help_structured_documents_title">Strukturált dokumentumok</string>
- <string name="formatting_help_structured_documents_body_1">Néha hasznos, ha különböző szintű címsorok vannak a dokumentumok strukturálásához. Indítsa el a sorokat %1$s-szal a címsorok létrehozásához. A(z) %2$s többszörös sorozata kisebb fejlécméretet jelöl.</string>
- <string name="formatting_help_structured_documents_body_2">Ez egy harmadik szintű cím</string>
- <string name="formatting_help_structured_documents_body_3">%1$s használható egészen %2$s hatig különböző fejlécméretekhez.</string>
- <string name="formatting_help_structured_documents_body_4">Ha valakit idézni szerete, használja a %1$s karaktert a sor előtt:</string>
+ <string name="formatting_help_structured_documents_body_1">Néha hasznos, ha különböző szintű címsorok vannak a dokumentumok strukturálásához. Címsorok létrehozásához kezdje a sorokat %1$s karakterrel. Több %2$s karakter kisebb címsorméretet jelöl.</string>
+ <string name="formatting_help_structured_documents_body_2">Ez egy harmadik szintű címsor</string>
+ <string name="formatting_help_structured_documents_body_3">Használhat egy %1$s karaktert égészen hat %2$s karakterig a különböző fejlécméretekhez.</string>
+ <string name="formatting_help_structured_documents_body_4">Ha valakit idézni szeretne, használja a %1$s karaktert a sor előtt:</string>
<string name="formatting_help_structured_documents_body_5">A képzelet sokkal fontosabb, mint a tudás. A tudás véges. A képzelet felöleli az egész világot.</string>
- <string name="formatting_help_structured_documents_body_6">- Albert Einstein</string>
+ <string name="formatting_help_structured_documents_body_6">– Albert Einstein</string>
<string name="formatting_help_code_title">Kód</string>
- <string name="formatting_help_code_body_1">A Markdown segítségével sokféleképpen lehet stílus kódolni. Ha beágyazott kódblokkok vannak, csomagolja be őket fordított idézőjelekbe:</string>
- <string name="formatting_help_code_body_2">A Markdown támogatja az úgynevezett kód keretezést is, amely több sort tesz lehetővé behúzás nélkül:</string>
- <string name="formatting_help_code_body_3">Ha pedig a szintaxis kiemelését szeretné használni, adja meg a nyelvet:</string>
+ <string name="formatting_help_code_body_1">A Markdown segítségével sokféleképpen jeleníthet meg forráskódot. Ha beágyazott kódblokkok vannak, csomagolja őket fordított idézőjelek közé:</string>
+ <string name="formatting_help_code_body_2">A Markdown támogatja az úgynevezett kódkeretezést is, amely több sort tesz lehetővé behúzás nélkül:</string>
+ <string name="formatting_help_code_body_3">Ha pedig szintaxiskiemelést szeretné használni, adja meg a nyelvet:</string>
<string name="formatting_help_tables_title">Táblázatok</string>
<!-- Column header of a sample table -->
@@ -240,7 +243,7 @@
<string name="error_action_open_battery_settings">Akkumulátorbeállítások</string>
<string name="error_action_open_deck_info">Alkalmazásinformációk megnyitása</string>
<string name="error_action_open_network">Hálózati beállítások</string>
-<string name="no_account_configured_yet">Nincs még fiók beállítva</string>
- <string name="no_other_accounts">Még egyetlen más fiókot se állított be.</string>
+ <string name="no_account_configured_yet">Nincs még fiók beállítva</string>
+ <string name="no_other_accounts">Még egyetlen más fiókot sem állított be.</string>
<string name="context_based_formatting">Környezetfüggő formázási felbukkanó menü</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 625a9f87..41c06489 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -45,7 +45,7 @@
<string name="settings_lock">Blocco applicazione (Beta)</string>
<string name="settings_lock_summary">Credenziali dispositivo</string>
<string name="settings_background_sync">Sincronizzazione in background</string>
- <string name="settings_prevent_screen_capture">Impedisci cattura schermo</string>
+ <string name="settings_prevent_screen_capture">Impedisci cattura dello schermo</string>
<string name="error_sync">Sincronizzazione non riuscita: %1$s</string>
<string name="error_synchronization">Sincronizzazione non riuscita</string>
@@ -246,4 +246,4 @@
<string name="no_account_configured_yet">Ancora nessun account configurato</string>
<string name="no_other_accounts">Non hai configurato ancora alcun account.</string>
<string name="context_based_formatting">Finestra di formattazione basata sul contesto</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 7e000e0c..17ec0dfd 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -233,4 +233,4 @@
<string name="error_action_open_network">네트워크 환경설정</string>
<string name="no_other_accounts">아직 다른 계정을 구성하지 않았습니다.</string>
<string name="context_based_formatting">컨텍스트 기반의 양식 팝오버</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index a3e4a857..802b553b 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -246,4 +246,4 @@
<string name="no_account_configured_yet">Nog geen account geconfigureerd</string>
<string name="no_other_accounts">U heeft nog geen andere accounts geconfigureerd.</string>
<string name="context_based_formatting">Context gebaseerde formatterings-popover</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 08689a8a..cd513c01 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Wszystkie notatki</string>
<string name="label_favorites">Ulubione</string>
<string name="action_create">Nowa notatka</string>
+ <string name="welcome_text">Witamy w %1$s</string>
<string name="action_settings">Ustawienia</string>
<string name="action_trashbin">Usunięte notatki</string>
<string name="action_search">Szukaj</string>
<string name="action_sorting_method">Metoda sortowania</string>
<string name="simple_cancel">Anuluj</string>
<string name="simple_edit">Edytuj</string>
+ <string name="simple_remove">Usuń</string>
<string name="action_edit_save">Zapisz</string>
<string name="simple_about">O aplikacji</string>
<string name="simple_link">Odnośnik</string>
@@ -253,5 +255,15 @@
<string name="error_action_open_network">Ustawienia sieci</string>
<string name="no_account_configured_yet">Nie skonfigurowano jeszcze konta</string>
<string name="no_other_accounts">Nie masz jeszcze skonfigurowanych żadnych innych kont.</string>
+ <string name="choose_account">Wybierz konto</string>
<string name="context_based_formatting">Okno podręczne formatowania opartego na kontekście</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Usunięcie konta %1$s, usunie również nieodwracalnie jedną niezsynchronizowaną zmianę.</item>
+ <item quantity="few">Usunięcie konta %1$s, usunie również nieodwracalnie %2$d niezsynchronizowane zmiany.</item>
+ <item quantity="many">Usunięcie konta %1$s, usunie również nieodwracalnie %2$d niezsynchronizowanych zmian.</item>
+ <item quantity="other">Usunięcie konta %1$s, usunie również nieodwracalnie %2$d niezsynchronizowanych zmian.</item>
+ </plurals>
+ <string name="remove_account">Usuń %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Aby dodać konto, musisz mieć połączenie z Internetem.</string>
</resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 57faf051..5fa5d5e9 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Todas as notas</string>
<string name="label_favorites">Favoritos</string>
<string name="action_create">Nova nota</string>
+ <string name="welcome_text">Bem-vindo ao %1$s</string>
<string name="action_settings">Configurações</string>
<string name="action_trashbin">Anotações excluídas</string>
<string name="action_search">Pesquisar</string>
<string name="action_sorting_method">Método de ordenação</string>
<string name="simple_cancel">Cancelar</string>
<string name="simple_edit">Editar</string>
+ <string name="simple_remove">Remover</string>
<string name="action_edit_save">Salvar</string>
<string name="simple_about">Sobre</string>
<string name="simple_link">Link</string>
@@ -245,5 +247,13 @@
<string name="error_action_open_network">Configurações da rede</string>
<string name="no_account_configured_yet">Nenhuma conta foi configurada</string>
<string name="no_other_accounts">Você ainda não configurou nenhuma outra conta.</string>
+ <string name="choose_account">Escolha a conta </string>
<string name="context_based_formatting">Formatação baseada no contexto popover</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Removendo a conta %1$s também excluirá uma alteração irrecuperável não foi sincronizada. </item>
+ <item quantity="other">Removendo a conta %1$s também irá deletar irrecuperávelmente %2$d mudanças não sincronizadas. </item>
+ </plurals>
+ <string name="remove_account">Remover %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Você deve estar conectado à Internet para adicionar uma conta. </string>
</resources>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index eb60c1ee..0b8dc0da 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Все заметки</string>
<string name="label_favorites">Избранные</string>
<string name="action_create">Новая заметка</string>
+ <string name="welcome_text">Добро пожаловать в %1$s</string>
<string name="action_settings">Настройки</string>
<string name="action_trashbin">Удалённые заметки</string>
<string name="action_search">Поиск</string>
<string name="action_sorting_method">Метод сортировки</string>
<string name="simple_cancel">Отмена</string>
<string name="simple_edit">Править</string>
+ <string name="simple_remove">Удалить</string>
<string name="action_edit_save">Сохранить</string>
<string name="simple_about">О программе</string>
<string name="simple_link">Ссылка</string>
@@ -42,7 +44,11 @@
<string name="settings_font_title">Моноширинный шрифт</string>
<string name="settings_font_size">Размер шрифта</string>
<string name="settings_wifi_only">Синхр. только по Wi-Fi</string>
+ <string name="settings_lock">Блокировка приложения (бета)</string>
+ <string name="settings_lock_summary">Данные устройства</string>
<string name="settings_background_sync">Синхр. в фоновом режиме</string>
+ <string name="settings_prevent_screen_capture">Запретить захват экрана</string>
+
<string name="error_sync">Сбой синхронизации: %1$s</string>
<string name="error_synchronization">Сбой синхронизации</string>
<string name="error_no_network">Нет подключения к сети</string>
@@ -111,6 +117,7 @@
<string name="error_dialog_title">О нет, что теперь? 🙁</string>
<string name="error_dialog_tip_token_mismatch_retry">Попробуйте принудительно закрыть приложение и запустить его снова. Это может быть связано с некорректным подключением к приложению Nextcloud.</string>
<string name="error_dialog_tip_token_mismatch_clear_storage">Если проблема повторилась, попробуйте для ее решения очистить хранилище обоих приложений: Nextcloud и Nextcloud Notes</string>
+ <string name="error_dialog_tip_clear_storage">Вы можете очистить хранилище, открыв информацию о приложении и выбрав Хранилище → Очистить хранилище. ⚠️ Предупреждение: это удалит еще не синхронизированные заметки!</string>
<string name="error_dialog_tip_files_outdated">Ваше приложение Nextcloud устарело. Установите новую версию с Play Store или F-Droid.</string>
<string name="error_dialog_tip_files_force_stop">Похоже что с вашим приложением Nextcloud что-то не так. Попробуйте принудительно остановить оба приложения Nextcloud и Nextcloud Notes. </string>
<string name="error_dialog_tip_files_delete_storage">Если принудительная остановка не поможет, попробуйте очистить хранилище обоих приложений.</string>
@@ -131,7 +138,9 @@
<string name="change_note_title">Изменить заголовок заметки</string>
<string name="menu_edit_title">Редактировать заголовок</string>
<string name="settings_branding">Брендирование</string>
+ <string name="settings_gridview">Вид сеткой</string>
<string name="simple_security">Безопасность</string>
+ <string name="appearance_and_behavior">Внешний вид и поведение</string>
<string name="simple_synchronization">Синхронизация</string>
<string name="simple_behavior">Режим</string>
<string name="manage_accounts">Управление аккаунтами</string>
@@ -191,6 +200,7 @@
<string name="formatting_help_codefence_inline" translateable="false">`%1$s`</string>
<string name="formatting_help_codefence_inline_escaped" translateable="false">\\`%1$s\\`</string>
<string name="formatting_help_codefence" translateable="false">```</string>
+ <string name="formatting_help_codefence_outer" translateable="false">````</string>
<string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
<string name="formatting_help_cbf_title">Контекстное форматирование</string>
<string name="formatting_help_cbf_body_1">Главная цель разработки приложения Notes - получить инструмент для быстрого создания заметок по текущим делам. Вы можете отформатировать текст, применяя синтаксис Markdown. Для приведенных ниже примеров удобно использовать клавиши быстрого доступа, что позволит выполнять форматирование ваших заметки без ввода самих символов форматирования.</string>
@@ -228,7 +238,15 @@
<string name="formatting_help_code_body_2">В Markdown также поддерживается так называемый обособленный блок кода, который содержит несколько строк без абзацного отступа:</string>
<string name="formatting_help_code_body_3">А если Вы хотите включить подсветку синтаксиса, укажите язык:</string>
+ <string name="formatting_help_tables_title">Таблицы</string>
+ <!-- Column header of a sample table -->
+ <string name="formatting_help_tables_column">Столбец %1d</string>
+ <!-- Table cell value of a sample table -->
+ <string name="formatting_help_tables_value">Значение %1d</string>
+
<string name="formatting_help_images_title">Изображения</string>
+ <string name="formatting_help_images_alt">Красивое изображение</string>
+
<string name="simple_other">Другой</string>
<string name="sort_last_modified">Сортировать по дате изменения</string>
<string name="sort_alphabetically">Сортировать по алфавиту</string>
@@ -237,4 +255,8 @@
<string name="error_action_open_network">Параметры сети</string>
<string name="no_account_configured_yet">Учётная запись пока не настроена</string>
<string name="no_other_accounts">Вы еще не настроили ни одного аккаунта</string>
+ <string name="choose_account">Выбрать учётную запись</string>
+ <string name="context_based_formatting">Всплывающее окно контекстного форматирования</string>
+ <string name="remove_account">Удалить %1$s</string>
+
</resources>
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index 45bd2b84..c50c6a67 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -16,7 +16,7 @@
<string name="simple_about">In contu de</string>
<string name="simple_link">Ligòngiu</string>
<string name="action_note_deleted">Cantzelladu %1$s</string>
- <string name="action_note_restored">Recuperadu %1$s</string>
+ <string name="action_note_restored">Ripristinadu %1$s</string>
<string name="action_undo">Annulla</string>
<string name="action_uncategorized">Sena categoria</string>
<string name="menu_delete">Cantzella</string>
@@ -240,4 +240,4 @@
<string name="no_account_configured_yet">Perunu contu configuradu ancora</string>
<string name="no_other_accounts">No as configuradu perunu contu ancora.</string>
<string name="context_based_formatting">Bentana de formatatzione basada subra de su cuntestu</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 2f77563a..5b369369 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Vse zabeležke</string>
<string name="label_favorites">Priljubljene</string>
<string name="action_create">Nova zabeležka</string>
+ <string name="welcome_text">Dobrodošli v %1$s</string>
<string name="action_settings">Nastavitve</string>
<string name="action_trashbin">Izbrisane zabeležke</string>
<string name="action_search">Poišči</string>
<string name="action_sorting_method">Način razvrščanja</string>
<string name="simple_cancel">Prekliči</string>
<string name="simple_edit">Uredi</string>
+ <string name="simple_remove">Odstrani</string>
<string name="action_edit_save">Shrani</string>
<string name="simple_about">O programu</string>
<string name="simple_link">Povezava</string>
@@ -42,7 +44,11 @@
<string name="settings_font_title">Pisava enotne širine</string>
<string name="settings_font_size">Velikost pisave</string>
<string name="settings_wifi_only">Usklajuj le na Wi-Fi</string>
+ <string name="settings_lock">Zaklep aplikacije (beta)</string>
+ <string name="settings_lock_summary">Poverila naprave</string>
<string name="settings_background_sync"> Usklajevanje v ozadju</string>
+ <string name="settings_prevent_screen_capture">Prepreči zajem zaslona</string>
+
<string name="error_sync">Usklajevanje je spodletelo: %1$s</string>
<string name="error_synchronization">Usklajevanje je spodletelo</string>
<string name="error_no_network">Ni omrežne povezave</string>
@@ -132,7 +138,9 @@
<string name="change_note_title">Spremeni naslov zabeležke</string>
<string name="menu_edit_title">Uredi naslov</string>
<string name="settings_branding">Prilagajanje oblikovanja</string>
+ <string name="settings_gridview">Mrežni pogled</string>
<string name="simple_security">Varnost</string>
+ <string name="appearance_and_behavior">Videz in obnašanje</string>
<string name="simple_synchronization">Usklajevanje</string>
<string name="simple_behavior">Obnašanje</string>
<string name="manage_accounts">Upravljanje z računi</string>
@@ -247,5 +255,15 @@
<string name="error_action_open_network">Nastavitve omrežja</string>
<string name="no_account_configured_yet">Ni nastavljenega nobenega računa</string>
<string name="no_other_accounts">Ni nastavljenega nobenega drugega računa</string>
+ <string name="choose_account">Izbor računa</string>
<string name="context_based_formatting">Pojavno okno vsebinskega oblikovanja</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Odstranjevanje računa %1$s trajno izbriše tudi neusklajeno spremembo.</item>
+ <item quantity="two">Odstranjevanje računa %1$s trajno izbriše tudi %2$d neusklajeni spremembi.</item>
+ <item quantity="few">Odstranjevanje računa %1$s trajno izbriše tudi %2$d neusklajene spremembe.</item>
+ <item quantity="other">Odstranjevanje računa %1$s trajno izbriše tudi %2$d neusklajenih sprememb.</item>
+ </plurals>
+ <string name="remove_account">Odstrani %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Za dodajanje računa mora biti vzpostavljena povezava z omrežjem.</string>
</resources>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index b99bc454..537b7c52 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Tum notlar</string>
<string name="label_favorites">Sık Kullanılanlar</string>
<string name="action_create">Not ekle</string>
+ <string name="welcome_text">%1$s uygulamasına hoş geldiniz</string>
<string name="action_settings">Ayarlar</string>
<string name="action_trashbin">Silinmiş notlar</string>
<string name="action_search">Arama</string>
<string name="action_sorting_method">Sıralama yöntemi</string>
<string name="simple_cancel">İptal</string>
<string name="simple_edit">Düzenle</string>
+ <string name="simple_remove">Sil</string>
<string name="action_edit_save">Kaydet</string>
<string name="simple_about">Hakkında</string>
<string name="simple_link">Bağlantı</string>
@@ -245,5 +247,13 @@
<string name="error_action_open_network">Ağ ayarları</string>
<string name="no_account_configured_yet">Henüz bir hesap yapılandırılmamış</string>
<string name="no_other_accounts">Henüz başka bir hesap yapılandırmamışsınız.</string>
+ <string name="choose_account">Hesap seçin</string>
<string name="context_based_formatting">Bağlam tabanlı biçimlendirme açılan penceresi</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">%1$s hesabı silindiğinde bir eşitlenmemiş değişiklik de geri alınamayacak şekilde silinecek.</item>
+ <item quantity="other">%1$s hesabı silindiğinde %2$d eşitlenmemiş değişiklik de geri alınamayacak şekilde silinecek.</item>
+ </plurals>
+ <string name="remove_account">%1$s sil</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Bir hesap ekleyebilmeniz için çalışan bir İnternet bağlantınız olmalı.</string>
</resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 48cbe39d..df7c0011 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">所有笔记</string>
<string name="label_favorites">收藏</string>
<string name="action_create">新建笔记</string>
+ <string name="welcome_text">欢迎来到 %1$s</string>
<string name="action_settings">设置</string>
<string name="action_trashbin">已删除的笔记</string>
<string name="action_search">搜索</string>
<string name="action_sorting_method">排序方法</string>
<string name="simple_cancel">取消</string>
<string name="simple_edit">编辑</string>
+ <string name="simple_remove">移除</string>
<string name="action_edit_save">保存</string>
<string name="simple_about">关于</string>
<string name="simple_link">链接</string>
@@ -241,5 +243,12 @@
<string name="error_action_open_network">网络设置</string>
<string name="no_account_configured_yet">尚未配置账户</string>
<string name="no_other_accounts">您尚未配置任何其他帐户。</string>
+ <string name="choose_account">选择一个账户</string>
<string name="context_based_formatting">基于上下文的格式弹出窗口 </string>
+ <plurals name="remove_account_message">
+ <item quantity="other">移除账户 %1$s 会同样不可恢复地删除 %2$d 未同步的更改</item>
+ </plurals>
+ <string name="remove_account">移除 %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">要添加账号,你必须连接到互联网。</string>
</resources>
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 05e59745..026d4682 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">所有筆記</string>
<string name="label_favorites">我的最愛</string>
<string name="action_create">新筆記</string>
+ <string name="welcome_text">歡迎來到 %1$s</string>
<string name="action_settings">設定</string>
<string name="action_trashbin">已刪除筆記</string>
<string name="action_search">搜尋</string>
<string name="action_sorting_method">排序方法</string>
<string name="simple_cancel">取消</string>
<string name="simple_edit">修改</string>
+ <string name="simple_remove">移除</string>
<string name="action_edit_save">儲存</string>
<string name="simple_about">關於</string>
<string name="simple_link">連結</string>
@@ -241,5 +243,12 @@
<string name="error_action_open_network">網路設定</string>
<string name="no_account_configured_yet">未有已設定的賬戶</string>
<string name="no_other_accounts">你尚未設定其它帳號。</string>
+ <string name="choose_account">選擇賬戶</string>
<string name="context_based_formatting">基於 context 的格式彈出框</string>
+ <plurals name="remove_account_message">
+ <item quantity="other">刪除賬戶 %1$s 還將刪除 %2$d 個無法恢復的非同步更改。</item>
+ </plurals>
+ <string name="remove_account">移除 %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">您必須連線到互聯網才能新增賬戶。</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index faef536b..892bd442 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,6 +13,7 @@
<string name="action_sorting_method">Sorting method</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_edit">Edit</string>
+ <string name="simple_remove">Remove</string>
<string name="action_edit_save">Save</string>
<string name="simple_about">About</string>
<string name="simple_link">Link</string>
@@ -51,6 +52,7 @@
<string name="error_sync">Synchronization failed: %1$s</string>
<string name="error_synchronization">Synchronization failed</string>
<string name="error_no_network">No network connection</string>
+ <string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_unknown">An unknown error has occurred.</string>
<string name="url_source" translatable="false">https://github.com/stefan-niedermann/nextcloud-notes</string>
@@ -298,9 +300,15 @@
<string name="no_other_accounts">You don\'t have configured any other accounts yet.</string>
<string name="choose_account">Choose account</string>
<string name="context_based_formatting">Context based formatting popover</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Removing the account %1$s will also delete irrecoverable one unsynchronized change.</item>
+ <item quantity="other">Removing the account %1$s will also delete irrecoverable %2$d unsynchronized changes.</item>
+ </plurals>
+ <string name="remove_account">Remove %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">You have to be connected to the internet in order to add an account.</string>
<string name="settings_notes_path">Set folder</string>
<string name="settings_file_suffix">File extension</string>
- <string name="remove_account">Remove account</string>
<string-array name="settings_file_suffixes">
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
index 1949dbad..7e8d49ea 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
@@ -1,6 +1,5 @@
package it.niedermann.owncloud.notes.persistence;
-import android.database.sqlite.SQLiteConstraintException;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -18,22 +17,11 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
-import java.util.Calendar;
-import java.util.List;
-
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.Capabilities;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = {Build.VERSION_CODES.P})
@@ -66,4 +54,25 @@ public class AccountDaoTest {
assertEquals("彼得", createdAccount.getUserName());
assertEquals("彼得@äöüß.example.com", createdAccount.getAccountName());
}
+
+ @Test
+ public void updateApiVersionFromNull() throws NextcloudHttpRequestFailedException {
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null))));
+ assertNull(account.getApiVersion());
+
+ assertEquals(0, db.getAccountDao().updateApiVersion(account.getId(), null));
+ assertEquals(1, db.getAccountDao().updateApiVersion(account.getId(), "[0.2]"));
+ assertEquals(0, db.getAccountDao().updateApiVersion(account.getId(), "[0.2]"));
+ }
+
+ @Test
+ public void updateApiVersionFromExisting() throws NextcloudHttpRequestFailedException {
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {data: {capabilities: {notes: {api_version: '[0.2]'}}}}}", null))));
+ assertEquals("[0.2]", account.getApiVersion());
+
+ assertEquals(0, db.getAccountDao().updateApiVersion(account.getId(), "[0.2]"));
+ assertEquals(1, db.getAccountDao().updateApiVersion(account.getId(), "[0.2, 1.0]"));
+ assertEquals(1, db.getAccountDao().updateApiVersion(account.getId(), null));
+ }
+
} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
index c59186c0..17df5122 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
@@ -53,7 +53,7 @@ public class NotesDaoTest {
.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class)
.allowMainThreadQueries()
.build();
- db.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null));
+ db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null)));
account = db.getAccountDao().getAccountByName("彼得@äöüß.example.com");
}
@@ -410,7 +410,7 @@ public class NotesDaoTest {
}
private Account setupSecondAccount() throws NextcloudHttpRequestFailedException {
- db.addAccount("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null));
+ db.getAccountDao().insert(new Account("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null)));
return db.getAccountDao().getAccountByName("test@example.org");
}
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTest.java
deleted file mode 100644
index 04536d32..00000000
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.content.Context;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
-import androidx.room.Room;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Map;
-
-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 static it.niedermann.owncloud.notes.persistence.NotesDatabaseTestUtil.getOrAwaitValue;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED;
-import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
-public class NotesDatabaseTest {
-
- @Rule
- public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
-
- @NonNull
- private Context context;
- private NotesDatabase db = null;
- private Account account = null;
- private Account secondAccount = null;
-
- @Before
- public void setupDB() throws NextcloudHttpRequestFailedException {
- context = ApplicationProvider.getApplicationContext();
- db = Room
- .inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class)
- .allowMainThreadQueries()
- .build();
-
- db.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":200,\"message\":\"OK\"},\"data\":{\"version\":{\"major\":18,\"minor\":0,\"micro\":4,\"string\":\"18.0.4\",\"edition\":\"\",\"extendedSupport\":false},\"capabilities\":{\"core\":{\"pollinterval\":60,\"webdav-root\":\"remote.php\\/webdav\"},\"bruteforce\":{\"delay\":0},\"files\":{\"bigfilechunking\":true,\"blacklisted_files\":[\".htaccess\"],\"directEditing\":{\"url\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/files\\/api\\/v1\\/directEditing\",\"etag\":\"ed2b141af2a39b0e42666952ba60988d\"},\"versioning\":true,\"undelete\":true},\"activity\":{\"apiv2\":[\"filters\",\"filters-api\",\"previews\",\"rich-strings\"]},\"ocm\":{\"enabled\":true,\"apiVersion\":\"1.0-proposal1\",\"endPoint\":\"https:\\/\\/efss.qloud.my\\/index.php\\/ocm\",\"resourceTypes\":[{\"name\":\"file\",\"shareTypes\":[\"user\",\"group\"],\"protocols\":{\"webdav\":\"\\/public.php\\/webdav\\/\"}}]},\"deck\":{\"version\":\"0.8.2\"},\"richdocuments\":{\"mimetypes\":[\"application\\/vnd.oasis.opendocument.text\",\"application\\/vnd.oasis.opendocument.spreadsheet\",\"application\\/vnd.oasis.opendocument.graphics\",\"application\\/vnd.oasis.opendocument.presentation\",\"application\\/vnd.lotus-wordpro\",\"application\\/vnd.visio\",\"application\\/vnd.wordperfect\",\"application\\/msonenote\",\"application\\/msword\",\"application\\/rtf\",\"text\\/rtf\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.document\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.template\",\"application\\/vnd.ms-word.document.macroEnabled.12\",\"application\\/vnd.ms-word.template.macroEnabled.12\",\"application\\/vnd.ms-excel\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.template\",\"application\\/vnd.ms-excel.sheet.macroEnabled.12\",\"application\\/vnd.ms-excel.template.macroEnabled.12\",\"application\\/vnd.ms-excel.addin.macroEnabled.12\",\"application\\/vnd.ms-excel.sheet.binary.macroEnabled.12\",\"application\\/vnd.ms-powerpoint\",\"application\\/vnd.openxmlformats-officedocument.presentationml.presentation\",\"application\\/vnd.openxmlformats-officedocument.presentationml.template\",\"application\\/vnd.openxmlformats-officedocument.presentationml.slideshow\",\"application\\/vnd.ms-powerpoint.addin.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.presentation.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.template.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.slideshow.macroEnabled.12\",\"text\\/csv\"],\"mimetypesNoDefaultOpen\":[\"image\\/svg+xml\",\"application\\/pdf\",\"text\\/plain\",\"text\\/spreadsheet\"],\"collabora\":[],\"direct_editing\":false,\"templates\":false,\"productName\":\"\\u5728\\u7ebf\\u534f\\u4f5c\"},\"dav\":{\"chunking\":\"1.0\"},\"files_sharing\":{\"api_enabled\":true,\"public\":{\"enabled\":true,\"password\":{\"enforced\":true,\"askForOptionalPassword\":false},\"expire_date\":{\"enabled\":true,\"days\":\"7\",\"enforced\":false},\"multiple_links\":true,\"expire_date_internal\":{\"enabled\":false},\"send_mail\":false,\"upload\":true,\"upload_files_drop\":true},\"resharing\":true,\"user\":{\"send_mail\":false,\"expire_date\":{\"enabled\":true}},\"group_sharing\":true,\"group\":{\"enabled\":true,\"expire_date\":{\"enabled\":true}},\"default_permissions\":31,\"federation\":{\"outgoing\":false,\"incoming\":false,\"expire_date\":{\"enabled\":true}},\"sharee\":{\"query_lookup_default\":false},\"sharebymail\":{\"enabled\":true,\"upload_files_drop\":{\"enabled\":true},\"password\":{\"enabled\":true},\"expire_date\":{\"enabled\":true}}},\"external\":{\"v1\":[\"sites\",\"device\",\"groups\",\"redirect\"]},\"notifications\":{\"ocs-endpoints\":[\"list\",\"get\",\"delete\",\"delete-all\",\"icons\",\"rich-strings\",\"action-web\"],\"push\":[\"devices\",\"object-data\",\"delete\"],\"admin-notifications\":[\"ocs\",\"cli\"]},\"password_policy\":{\"minLength\":8,\"enforceNonCommonPassword\":true,\"enforceNumericCharacters\":false,\"enforceSpecialCharacters\":false,\"enforceUpperLowerCase\":false,\"api\":{\"generate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/generate\",\"validate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/validate\"}},\"theming\":{\"name\":\"QloudData\",\"url\":\"https:\\/\\/www.qloud.my\\/qloud-data\\/\",\"slogan\":\"Powered by NextCloud\",\"color\":\"#1E4164\",\"color-text\":\"#ffffff\",\"color-element\":\"#1E4164\",\"logo\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"background\":\"https:\\/\\/efss.qloud.my\\/core\\/img\\/background.png?v=47\",\"background-plain\":false,\"background-default\":true,\"logoheader\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"favicon\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\"},\"registration\":{\"enabled\":true,\"apiRoot\":\"\\/ocs\\/v2.php\\/apps\\/registration\\/api\\/v1\\/\",\"apiLevel\":\"v1\"}}}}}", null));
- account = db.getAccountDao().getAccountByName("彼得@äöüß.example.com");
-
- db.addAccount("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null));
- secondAccount = db.getAccountDao().getAccountByName("test@example.org");
-
- Arrays.stream(new Note[]{
- new Note(1, 1001L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, VOID, account.getId(), "", 0),
- new Note(2, null, Calendar.getInstance(), "T", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0),
- new Note(3, 1003L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0),
- new Note(4, null, Calendar.getInstance(), "T", "C", "Music", false, null, VOID, account.getId(), "", 0),
- new Note(5, 1005L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_EDITED, account.getId(), "", 0),
- new Note(6, 1006L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_DELETED, account.getId(), "", 0),
- new Note(7, null, Calendar.getInstance(), "T", "C", "Music", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0),
- new Note(8, 1008L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0),
- new Note(9, 1009L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_DELETED, secondAccount.getId(), "", 0)
- }).forEach(note -> db.getNoteDao().addNote(note));
- }
-
- @After
- public void closeDb() {
- db.close();
- }
-
- @Test
- public void testGetIdMap() {
- final Map<Long, Long> idMapOfFirstAccount = db.getIdMap(account.getId());
- assertEquals(3, idMapOfFirstAccount.size());
- assertEquals(Long.valueOf(1L), idMapOfFirstAccount.get(1001L));
- assertEquals(Long.valueOf(3L), idMapOfFirstAccount.get(1003L));
- assertEquals(Long.valueOf(5L), idMapOfFirstAccount.get(1005L));
-
- final Map<Long, Long> idMapOfSecondAccount = db.getIdMap(secondAccount.getId());
- assertEquals(1, idMapOfSecondAccount.size());
- assertEquals(Long.valueOf(8L), idMapOfSecondAccount.get(1008L));
- }
-
- @Test
- public void testAddAccount() throws NextcloudHttpRequestFailedException, InterruptedException {
- final Account createdAccount = getOrAwaitValue(db.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null)));
- assertEquals("https://äöüß.example.com", createdAccount.getUrl());
- assertEquals("彼得", createdAccount.getUserName());
- assertEquals("彼得@äöüß.example.com", createdAccount.getAccountName());
- }
-
- @Test
- public void testAddNote() {
- final Note localNote = new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123");
- localNote.setId(99);
- final Note createdNoteFromLocal = db.addNote(account.getId(), localNote);
- assertEquals(LOCAL_EDITED, createdNoteFromLocal.getStatus());
- assertEquals("MyContent", createdNoteFromLocal.getExcerpt());
-
- final Note createdNoteFromRemote = db.addNote(account.getId(), new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123"));
- assertEquals(VOID, createdNoteFromRemote.getStatus());
- assertEquals("MyContent", createdNoteFromRemote.getExcerpt());
- }
-
- @Test
- public void updateApiVersion() {
- assertThrows(IllegalArgumentException.class, () -> db.updateApiVersion(account.getId(), ""));
- assertThrows(IllegalArgumentException.class, () -> db.updateApiVersion(account.getId(), "asdf"));
- assertThrows(IllegalArgumentException.class, () -> db.updateApiVersion(account.getId(), "{}"));
-
- db.updateApiVersion(account.getId(), null);
- assertNull(db.getAccountDao().getAccountById(account.getId()).getApiVersion());
- db.updateApiVersion(account.getId(), "[]");
- assertNull(db.getAccountDao().getAccountById(account.getId()).getApiVersion());
-
- db.updateApiVersion(account.getId(), "[1.0]");
- assertEquals("[1.0]", db.getAccountDao().getAccountById(account.getId()).getApiVersion());
- db.updateApiVersion(account.getId(), "[0.2, 1.0]");
- assertEquals("[0.2, 1.0]", db.getAccountDao().getAccountById(account.getId()).getApiVersion());
-
- // TODO is this really indented?
- db.updateApiVersion(account.getId(), "[0.2, abc]");
- assertEquals("[0.2, abc]", db.getAccountDao().getAccountById(account.getId()).getApiVersion());
- }
-
- @Test
- @Ignore("Need to find a way to stub deleteAndSync method")
- public void moveNoteToAnotherAccount() throws InterruptedException {
- final Note noteToMove = db.getNoteDao().getNoteById(1);
- assertEquals(3, db.getNoteDao().getLocalModifiedNotes(secondAccount.getId()).size());
- final Note movedNote = getOrAwaitValue(db.moveNoteToAnotherAccount(secondAccount, noteToMove));
- assertEquals(4, db.getNoteDao().getLocalModifiedNotes(secondAccount.getId()).size());
- assertEquals(LOCAL_EDITED, movedNote.getStatus());
- // TODO assert deleteAndSync has been called
- }
-} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java
new file mode 100644
index 00000000..11c58dde
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java
@@ -0,0 +1,187 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.room.Room;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+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.IResponseCallback;
+
+import static it.niedermann.owncloud.notes.persistence.NotesDatabaseTestUtil.getOrAwaitValue;
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED;
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
+public class NotesRepositoryTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private NotesRepository repo = null;
+ private Account account = null;
+ private Account secondAccount = null;
+ private NotesDatabase db;
+
+ @Before
+ public void setupDB() throws NextcloudHttpRequestFailedException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ final Context context = ApplicationProvider.getApplicationContext();
+ db = Room
+ .inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class)
+ .allowMainThreadQueries()
+ .build();
+
+ final Constructor<NotesRepository> constructor = NotesRepository.class.getDeclaredConstructor(Context.class, NotesDatabase.class, ExecutorService.class);
+ constructor.setAccessible(true);
+ repo = constructor.newInstance(context, db, MoreExecutors.newDirectExecutorService());
+
+ repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":200,\"message\":\"OK\"},\"data\":{\"version\":{\"major\":18,\"minor\":0,\"micro\":4,\"string\":\"18.0.4\",\"edition\":\"\",\"extendedSupport\":false},\"capabilities\":{\"core\":{\"pollinterval\":60,\"webdav-root\":\"remote.php\\/webdav\"},\"bruteforce\":{\"delay\":0},\"files\":{\"bigfilechunking\":true,\"blacklisted_files\":[\".htaccess\"],\"directEditing\":{\"url\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/files\\/api\\/v1\\/directEditing\",\"etag\":\"ed2b141af2a39b0e42666952ba60988d\"},\"versioning\":true,\"undelete\":true},\"activity\":{\"apiv2\":[\"filters\",\"filters-api\",\"previews\",\"rich-strings\"]},\"ocm\":{\"enabled\":true,\"apiVersion\":\"1.0-proposal1\",\"endPoint\":\"https:\\/\\/efss.qloud.my\\/index.php\\/ocm\",\"resourceTypes\":[{\"name\":\"file\",\"shareTypes\":[\"user\",\"group\"],\"protocols\":{\"webdav\":\"\\/public.php\\/webdav\\/\"}}]},\"deck\":{\"version\":\"0.8.2\"},\"richdocuments\":{\"mimetypes\":[\"application\\/vnd.oasis.opendocument.text\",\"application\\/vnd.oasis.opendocument.spreadsheet\",\"application\\/vnd.oasis.opendocument.graphics\",\"application\\/vnd.oasis.opendocument.presentation\",\"application\\/vnd.lotus-wordpro\",\"application\\/vnd.visio\",\"application\\/vnd.wordperfect\",\"application\\/msonenote\",\"application\\/msword\",\"application\\/rtf\",\"text\\/rtf\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.document\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.template\",\"application\\/vnd.ms-word.document.macroEnabled.12\",\"application\\/vnd.ms-word.template.macroEnabled.12\",\"application\\/vnd.ms-excel\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.template\",\"application\\/vnd.ms-excel.sheet.macroEnabled.12\",\"application\\/vnd.ms-excel.template.macroEnabled.12\",\"application\\/vnd.ms-excel.addin.macroEnabled.12\",\"application\\/vnd.ms-excel.sheet.binary.macroEnabled.12\",\"application\\/vnd.ms-powerpoint\",\"application\\/vnd.openxmlformats-officedocument.presentationml.presentation\",\"application\\/vnd.openxmlformats-officedocument.presentationml.template\",\"application\\/vnd.openxmlformats-officedocument.presentationml.slideshow\",\"application\\/vnd.ms-powerpoint.addin.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.presentation.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.template.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.slideshow.macroEnabled.12\",\"text\\/csv\"],\"mimetypesNoDefaultOpen\":[\"image\\/svg+xml\",\"application\\/pdf\",\"text\\/plain\",\"text\\/spreadsheet\"],\"collabora\":[],\"direct_editing\":false,\"templates\":false,\"productName\":\"\\u5728\\u7ebf\\u534f\\u4f5c\"},\"dav\":{\"chunking\":\"1.0\"},\"files_sharing\":{\"api_enabled\":true,\"public\":{\"enabled\":true,\"password\":{\"enforced\":true,\"askForOptionalPassword\":false},\"expire_date\":{\"enabled\":true,\"days\":\"7\",\"enforced\":false},\"multiple_links\":true,\"expire_date_internal\":{\"enabled\":false},\"send_mail\":false,\"upload\":true,\"upload_files_drop\":true},\"resharing\":true,\"user\":{\"send_mail\":false,\"expire_date\":{\"enabled\":true}},\"group_sharing\":true,\"group\":{\"enabled\":true,\"expire_date\":{\"enabled\":true}},\"default_permissions\":31,\"federation\":{\"outgoing\":false,\"incoming\":false,\"expire_date\":{\"enabled\":true}},\"sharee\":{\"query_lookup_default\":false},\"sharebymail\":{\"enabled\":true,\"upload_files_drop\":{\"enabled\":true},\"password\":{\"enabled\":true},\"expire_date\":{\"enabled\":true}}},\"external\":{\"v1\":[\"sites\",\"device\",\"groups\",\"redirect\"]},\"notifications\":{\"ocs-endpoints\":[\"list\",\"get\",\"delete\",\"delete-all\",\"icons\",\"rich-strings\",\"action-web\"],\"push\":[\"devices\",\"object-data\",\"delete\"],\"admin-notifications\":[\"ocs\",\"cli\"]},\"password_policy\":{\"minLength\":8,\"enforceNonCommonPassword\":true,\"enforceNumericCharacters\":false,\"enforceSpecialCharacters\":false,\"enforceUpperLowerCase\":false,\"api\":{\"generate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/generate\",\"validate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/validate\"}},\"theming\":{\"name\":\"QloudData\",\"url\":\"https:\\/\\/www.qloud.my\\/qloud-data\\/\",\"slogan\":\"Powered by NextCloud\",\"color\":\"#1E4164\",\"color-text\":\"#ffffff\",\"color-element\":\"#1E4164\",\"logo\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"background\":\"https:\\/\\/efss.qloud.my\\/core\\/img\\/background.png?v=47\",\"background-plain\":false,\"background-default\":true,\"logoheader\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"favicon\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\"},\"registration\":{\"enabled\":true,\"apiRoot\":\"\\/ocs\\/v2.php\\/apps\\/registration\\/api\\/v1\\/\",\"apiLevel\":\"v1\"}}}}}", null), new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account result) {
+
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ fail();
+ }
+ });
+ account = repo.getAccountByName("彼得@äöüß.example.com");
+
+ repo.addAccount("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null), new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account result) {
+
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ fail();
+ }
+ });
+ secondAccount = repo.getAccountByName("test@example.org");
+
+ Arrays.stream(new Note[]{
+ new Note(1, 1001L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, VOID, account.getId(), "", 0),
+ new Note(2, null, Calendar.getInstance(), "T", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0),
+ new Note(3, 1003L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0),
+ new Note(4, null, Calendar.getInstance(), "T", "C", "Music", false, null, VOID, account.getId(), "", 0),
+ new Note(5, 1005L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_EDITED, account.getId(), "", 0),
+ new Note(6, 1006L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_DELETED, account.getId(), "", 0),
+ new Note(7, null, Calendar.getInstance(), "T", "C", "Music", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0),
+ new Note(8, 1008L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0),
+ new Note(9, 1009L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_DELETED, secondAccount.getId(), "", 0)
+ }).forEach(note -> db.getNoteDao().addNote(note));
+ }
+
+ @After
+ public void closeDb() {
+ db.close();
+ }
+
+ @Test
+ public void testGetIdMap() {
+ final Map<Long, Long> idMapOfFirstAccount = repo.getIdMap(account.getId());
+ assertEquals(3, idMapOfFirstAccount.size());
+ assertEquals(Long.valueOf(1L), idMapOfFirstAccount.get(1001L));
+ assertEquals(Long.valueOf(3L), idMapOfFirstAccount.get(1003L));
+ assertEquals(Long.valueOf(5L), idMapOfFirstAccount.get(1005L));
+
+ final Map<Long, Long> idMapOfSecondAccount = repo.getIdMap(secondAccount.getId());
+ assertEquals(1, idMapOfSecondAccount.size());
+ assertEquals(Long.valueOf(8L), idMapOfSecondAccount.get(1008L));
+ }
+
+ @Test
+ public void testAddAccount() throws NextcloudHttpRequestFailedException, InterruptedException {
+ repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null), new IResponseCallback<Account>() {
+ @Override
+ public void onSuccess(Account createdAccount) {
+ assertEquals("https://äöüß.example.com", createdAccount.getUrl());
+ assertEquals("彼得", createdAccount.getUserName());
+ assertEquals("彼得@äöüß.example.com", createdAccount.getAccountName());
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ fail();
+ }
+ });
+ }
+
+ @Test
+ public void testAddNote() {
+ final Note localNote = new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123");
+ localNote.setId(99);
+ final Note createdNoteFromLocal = repo.addNote(account.getId(), localNote);
+ assertEquals(LOCAL_EDITED, createdNoteFromLocal.getStatus());
+ assertEquals("MyContent", createdNoteFromLocal.getExcerpt());
+
+ final Note createdNoteFromRemote = repo.addNote(account.getId(), new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123"));
+ assertEquals(VOID, createdNoteFromRemote.getStatus());
+ assertEquals("MyContent", createdNoteFromRemote.getExcerpt());
+ }
+
+ @Test
+ public void updateApiVersion() {
+ assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), ""));
+ assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "asdf"));
+ assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "{}"));
+
+ repo.updateApiVersion(account.getId(), null);
+ assertNull(repo.getAccountById(account.getId()).getApiVersion());
+ repo.updateApiVersion(account.getId(), "[]");
+ assertNull(repo.getAccountById(account.getId()).getApiVersion());
+
+ repo.updateApiVersion(account.getId(), "[1.0]");
+ assertEquals("[1.0]", repo.getAccountById(account.getId()).getApiVersion());
+ repo.updateApiVersion(account.getId(), "[0.2, 1.0]");
+ assertEquals("[0.2, 1.0]", repo.getAccountById(account.getId()).getApiVersion());
+
+ // TODO is this really indented?
+ repo.updateApiVersion(account.getId(), "[0.2, abc]");
+ assertEquals("[0.2, abc]", repo.getAccountById(account.getId()).getApiVersion());
+ }
+
+ @Test
+ @Ignore("Need to find a way to stub deleteAndSync method")
+ public void moveNoteToAnotherAccount() throws InterruptedException {
+ final Note noteToMove = repo.getNoteById(1);
+ assertEquals(3, repo.getLocalModifiedNotes(secondAccount.getId()).size());
+ final Note movedNote = getOrAwaitValue(repo.moveNoteToAnotherAccount(secondAccount, noteToMove));
+ assertEquals(4, repo.getLocalModifiedNotes(secondAccount.getId()).size());
+ assertEquals(LOCAL_EDITED, movedNote.getStatus());
+ // TODO assert deleteAndSync has been called
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java
index 6415bb39..5a30bd2a 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java
@@ -48,8 +48,8 @@ public class CapabilitiesTest {
final Capabilities capabilities = new Capabilities(response, null);
assertNull(capabilities.getETag());
assertNull(capabilities.getApiVersion());
- assertEquals(Integer.valueOf(Color.parseColor("#1E4164")), capabilities.getColor());
- assertEquals(Integer.valueOf(Color.parseColor("#ffffff")), capabilities.getTextColor());
+ assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
}
@Test
@@ -86,8 +86,8 @@ public class CapabilitiesTest {
final Capabilities capabilities = new Capabilities(response, null);
assertNull(capabilities.getETag());
assertEquals("1.0", capabilities.getApiVersion());
- assertEquals(Integer.valueOf(Color.parseColor("#1E4164")), capabilities.getColor());
- assertEquals(Integer.valueOf(Color.parseColor("#ffffff")), capabilities.getTextColor());
+ assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
}
@Test
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
index a0e1f9c0..7a264fdd 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
@@ -9,6 +9,8 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
+import it.niedermann.android.markdown.MarkdownUtil;
+
/**
* Tests the NoteUtil
* Created by stefan on 06.10.15.
@@ -42,6 +44,9 @@ public class NoteUtilTest extends TestCase {
assertEquals("Test", NoteUtil.generateNoteTitle("Test\nFoo"));
assertEquals("Test", NoteUtil.generateNoteTitle("\nTest"));
assertEquals("Test", NoteUtil.generateNoteTitle("\n\nTest"));
+
+ // https://github.com/stefan-niedermann/nextcloud-notes/issues/1104
+ assertEquals("2021-03-24 - Example title", MarkdownUtil.removeMarkdown("2021-03-24 - Example title"));
}
@Test
diff --git a/fastlane/metadata/android/en-US/changelogs/3004000.txt b/fastlane/metadata/android/en-US/changelogs/3004000.txt
new file mode 100644
index 00000000..1167e4b5
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004000.txt
@@ -0,0 +1 @@
+- ⚙️ Migrate from SQLiteOpenHelper to Room database (#831) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004001.txt b/fastlane/metadata/android/en-US/changelogs/3004001.txt
new file mode 100644
index 00000000..1a2e41bf
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004001.txt
@@ -0,0 +1,5 @@
+- ⚠️️ Display confirm dialog when deleting an account with unsynchronized changes (#989) - by @AlpAcA0072
+- ➖ Allow dashes in note titles (#1104)
+- 🌐 Support links in tables (#1115)
+- 🔌 Handle offline state when adding accounts (#1014)
+- 🐞 Fix parallel synchronization issues which can lead to duplicate primary keys \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004002.txt b/fastlane/metadata/android/en-US/changelogs/3004002.txt
new file mode 100644
index 00000000..3eb3160a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004002.txt
@@ -0,0 +1,2 @@
+- 🌐 Enhanced linkifying - by @Cui-Yusong
+- ⚙️ Use Retrofit for API calls (#1167) \ No newline at end of file
diff --git a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java b/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java
index bcd28328..7c3f9774 100644
--- a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java
+++ b/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java
@@ -299,20 +299,20 @@ public class MarkdownUtilTest extends TestCase {
assertEquals(7, MarkdownUtil.insertLink(builder, 6, 25, null));
assertEquals("Lorem [](https://example.com) dolor sit amet.", builder.toString());
- // TODO Add link without clipboardUrl to empty selection before space character
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, null));
-// assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
+ // Add link without clipboardUrl to empty selection before space character
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, null));
+ assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
- // TODO Add link without clipboardUrl to empty selection after space character
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, null));
-// assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
+ // Add link without clipboardUrl to empty selection after space character
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, null));
+ assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
- // TODO Add link without clipboardUrl to empty selection in word
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(20, MarkdownUtil.insertLink(builder, 14, 14, null));
-// assertEquals("Lorem ipsum [dolor]() sit amet.", builder.toString());
+ // Add link without clipboardUrl to empty selection in word
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(20, MarkdownUtil.insertLink(builder, 14, 14, null));
+ assertEquals("Lorem ipsum [dolor]() sit amet.", builder.toString());
// Add link with clipboardUrl to normal text
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
@@ -324,20 +324,100 @@ public class MarkdownUtilTest extends TestCase {
assertEquals(46, MarkdownUtil.insertLink(builder, 6, 25, "https://example.de"));
assertEquals("Lorem [https://example.com](https://example.de) dolor sit amet.", builder.toString());
- // TODO Add link with clipboardUrl to empty selection before space character
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, "https://example.de"));
-// assertEquals("Lorem ipsum []("https://example.de") dolor sit amet.", builder.toString());
+ // Add link with clipboardUrl to empty selection before space character
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, "https://example.de"));
+ assertEquals("Lorem ipsum [](https://example.de) dolor sit amet.", builder.toString());
- // TODO Add link with clipboardUrl to empty selection after space character
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, "https://example.de"));
-// assertEquals("Lorem ipsum []("https://example.de") dolor sit amet.", builder.toString());
+ // Add link with clipboardUrl to empty selection after space character
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, "https://example.de"));
+ assertEquals("Lorem ipsum [](https://example.de) dolor sit amet.", builder.toString());
- // TODO Add link with clipboardUrl to empty selection in word
-// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
-// assertEquals(18, MarkdownUtil.insertLink(builder, 14, 14, "https://example.de"));
-// assertEquals("Lorem ipsum [dolor]("https://example.de") sit amet.", builder.toString());
+ // Add link with clipboardUrl to empty selection in word
+ builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
+ assertEquals(38, MarkdownUtil.insertLink(builder, 14, 14, "https://example.de"));
+ assertEquals("Lorem ipsum [dolor](https://example.de) sit amet.", builder.toString());
+
+ // Add link without clipboardUrl to empty selection on empty text
+ builder = new SpannableStringBuilder("");
+ assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, null));
+ assertEquals("[]()", builder.toString());
+
+ // Add link without clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, null));
+ assertEquals("[]() ", builder.toString());
+
+ // Add link without clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(4, MarkdownUtil.insertLink(builder, 0, 1, null));
+ assertEquals("[ ]()", builder.toString());
+
+ // Add link without clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, null));
+ assertEquals(" []()", builder.toString());
+
+ // Add link without clipboardUrl to empty selection on only spaces
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, null));
+ assertEquals(" []() ", builder.toString());
+
+ // Add link without clipboardUrl to empty selection in word with trailing and leading spaces
+ builder = new SpannableStringBuilder(" Lorem ");
+ assertEquals(10, MarkdownUtil.insertLink(builder, 5, 5, null));
+ assertEquals(" [Lorem]() ", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on empty text
+ builder = new SpannableStringBuilder("");
+ assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
+ assertEquals("[](https://www.example.com)", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
+ assertEquals("[](https://www.example.com) ", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(27, MarkdownUtil.insertLink(builder, 0, 1, "https://www.example.com"));
+ assertEquals("[ ](https://www.example.com)", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on only space text
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
+ assertEquals(" [](https://www.example.com)", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on one character
+ builder = new SpannableStringBuilder("a");
+ assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
+ assertEquals("[](https://www.example.com)a", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on one character
+ builder = new SpannableStringBuilder("a");
+ assertEquals(27, MarkdownUtil.insertLink(builder, 0, 1, "https://www.example.com"));
+ assertEquals("[a](https://www.example.com)", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on one character
+ builder = new SpannableStringBuilder("a");
+ assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
+ assertEquals("a[](https://www.example.com)", builder.toString());
+
+ // Add link with clipboardUrl to empty selection on only spaces
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
+ assertEquals(" [](https://www.example.com) ", builder.toString());
+
+ // Add link with clipboardUrl to empty selection in word with trailing and leading spaces
+ builder = new SpannableStringBuilder(" Lorem ");
+ assertEquals(33, MarkdownUtil.insertLink(builder, 5, 5, "https://www.example.com"));
+ assertEquals(" [Lorem](https://www.example.com) ", builder.toString());
+
+ // Add link with clipboardUrl to selection in word with trailing and leading spaces
+ builder = new SpannableStringBuilder(" Lorem ");
+ assertEquals(33, MarkdownUtil.insertLink(builder, 2, 7, "https://www.example.com"));
+ assertEquals(" [Lorem](https://www.example.com) ", builder.toString());
}
@Test
@@ -660,8 +740,10 @@ public class MarkdownUtilTest extends TestCase {
assertEquals("Title", MarkdownUtil.removeMarkdown("# Title"));
assertEquals("Aufzählung", MarkdownUtil.removeMarkdown("* Aufzählung"));
// assertEquals("Foo Link Bar", MarkdownUtil.removeMarkdown("Foo [Link](https://example.com) Bar"));
-
assertFalse(MarkdownUtil.removeMarkdown("- [ ] Test").contains("- [ ]"));
assertTrue(MarkdownUtil.removeMarkdown("- [ ] Test").endsWith("Test"));
+
+ // https://github.com/stefan-niedermann/nextcloud-notes/issues/1104
+ assertEquals("2021-03-24 - Example text", MarkdownUtil.removeMarkdown("2021-03-24 - Example text"));
}
} \ No newline at end of file
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
index 99575914..097acd76 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
@@ -21,9 +21,7 @@ import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.syntax.text.TextFactory;
import com.yydcdut.rxmarkdown.RxMarkdown;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
+import java.util.Arrays;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -315,10 +313,50 @@ public class MarkdownUtil {
*
* @return the new cursor position
*/
+ // CS304 issue link: https://github.com/stefan-niedermann/nextcloud-notes/issues/1186
public static int insertLink(@NonNull Editable editable, int selectionStart, int selectionEnd, @Nullable String clipboardUrl) {
if (selectionStart == selectionEnd) {
- editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
- return selectionStart + 1;
+ if (selectionStart>0 && selectionEnd<editable.length()) {
+ char start = editable.charAt(selectionStart - 1);
+ char end = editable.charAt(selectionEnd);
+ if (start == ' ' || end == ' ') {
+ if (start != ' ') {
+ editable.insert(selectionStart, " ");
+ selectionStart += 1;
+ }
+ if (end != ' ') {
+ editable.insert(selectionEnd, " ");
+ }
+ editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
+ if (clipboardUrl != null) {
+ selectionEnd += clipboardUrl.length();
+ }
+ return selectionStart + 1;
+
+ } else {
+ while (start != ' ') {
+ selectionStart--;
+ start = editable.charAt(selectionStart);
+ }
+ selectionStart++;
+ while (end != ' ') {
+ selectionEnd++;
+ end = editable.charAt(selectionEnd);
+ }
+ selectionEnd++;
+ editable.insert(selectionStart, "[");
+ editable.insert(selectionEnd, "](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
+ if (clipboardUrl != null) {
+ selectionEnd += clipboardUrl.length();
+ }
+ return selectionEnd + 2;
+ }
+ }
+ else {
+ editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
+ return selectionStart + 1;
+ }
+
} else {
final boolean textToFormatIsLink = TextUtils.indexOf(editable.subSequence(selectionStart, selectionEnd), "http") == 0;
if (textToFormatIsLink) {
@@ -446,9 +484,11 @@ public class MarkdownUtil {
// TODO maybe we can utilize the markwon renderer?
for (EListType listType : EListType.values()) {
- s = s.replace(listType.checkboxChecked, "");
- s = s.replace(listType.checkboxUnchecked, "");
- s = s.replace(listType.listSymbolWithTrailingSpace, "");
+ for (String item : Arrays.asList(listType.checkboxChecked, listType.checkboxUnchecked, listType.listSymbolWithTrailingSpace)) {
+ if (s.startsWith(item)) {
+ s = s.substring(item.length());
+ }
+ }
}
s = PATTERN_LISTS.matcher(s).replaceAll("");
s = PATTERN_HEADINGS.matcher(s).replaceAll("$1");
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
index 836a8536..d41681c3 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
@@ -22,12 +22,14 @@ import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
+import io.noties.markwon.ext.tables.TableAwareMovementMethod;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.image.DefaultDownScalingMediaDecoder;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
+import io.noties.markwon.movement.MovementMethodPlugin;
import io.noties.markwon.simple.ext.SimpleExtPlugin;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
@@ -86,6 +88,7 @@ public class MarkwonMarkdownViewer extends AppCompatTextView implements Markdown
.usePlugin(TablePlugin.create(context))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(LinkifyPlugin.create(true))
+ .usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.usePlugin(LinkClickInterceptorPlugin.create())
.usePlugin(ImagesPlugin.create(plugin -> plugin.defaultMediaDecoder(DefaultDownScalingMediaDecoder.create(context.getResources().getDisplayMetrics().widthPixels, 0))))
.usePlugin(SoftBreakAddsNewLinePlugin.create())