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:
Diffstat (limited to 'app/src/main/java/it')
-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
47 files changed, 2154 insertions, 1838 deletions
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");