diff options
author | Stefan Niedermann <info@niedermann.it> | 2021-04-23 20:29:30 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2021-04-26 11:47:11 +0300 |
commit | d40f5dfa76601f154dc8b8a2ed9993ba09d796ad (patch) | |
tree | 437b8bfdf38c07f8eca782ffb53d08017a6e73f0 | |
parent | f95d91c272b986fc9489a3bf3e3cf3feeb171eb4 (diff) |
#1170 Migrate NotesServerSyncHelper to NotesRepository
25 files changed, 1282 insertions, 969 deletions
diff --git a/app/build.gradle b/app/build.gradle index d5944241..a1f7d7da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,6 +102,7 @@ dependencies { // Testing testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.9.0' testImplementation 'org.robolectric:robolectric:4.5.1' testImplementation 'androidx.test:core:1.3.0' testImplementation 'androidx.test.ext:junit:1.1.2' diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index 79467d86..f5643d86 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -21,7 +21,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding; import it.niedermann.owncloud.notes.manageaccounts.ManageAccountsActivity; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import static it.niedermann.owncloud.notes.branding.BrandingUtil.applyBrandToLayerDrawable; @@ -33,7 +33,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { private static final String KEY_CURRENT_ACCOUNT_ID = "current_account_id"; - private NotesDatabase db; + private NotesRepository repo; private DialogAccountSwitcherBinding binding; private AccountSwitcherListener accountSwitcherListener; private long currentAccountId; @@ -55,7 +55,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { this.currentAccountId = args.getLong(KEY_CURRENT_ACCOUNT_ID); } - db = NotesDatabase.getInstance(requireActivity()); + repo = NotesRepository.getInstance(requireContext()); } @NonNull @@ -63,7 +63,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater()); - final LiveData<Account> account$ = db.getAccountDao().getAccountById$(currentAccountId); + final LiveData<Account> account$ = repo.getAccountById$(currentAccountId); account$.observe(requireActivity(), (currentLocalAccount) -> { account$.removeObservers(requireActivity()); @@ -81,7 +81,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { dismiss(); })); binding.accountsList.setAdapter(adapter); - final LiveData<List<Account>> localAccounts$ = db.getAccountDao().getAccounts$(); + final LiveData<List<Account>> localAccounts$ = repo.getAccounts$(); localAccounts$.observe(requireActivity(), (localAccounts) -> { localAccounts$.removeObservers(requireActivity()); for (Account localAccount : localAccounts) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index 383d9e1b..1b4b8289 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(); @@ -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/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java index 04b30d8d..abcf68e3 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,7 +6,7 @@ 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; @@ -15,14 +15,14 @@ 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); + return repo.addAccount(url, username, accountName, capabilities); } } 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 77104856..2e43d78d 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 @@ -34,8 +34,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; @@ -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) )); }); @@ -390,22 +389,21 @@ public class MainViewModel extends AndroidViewModel { */ 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()); + 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) { @@ -416,7 +414,7 @@ public class MainViewModel extends AndroidViewModel { } } } 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.")); @@ -431,15 +429,14 @@ public class MainViewModel extends AndroidViewModel { 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); + 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 +446,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 +474,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 +482,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 +494,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 +506,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,7 +519,7 @@ 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); } @@ -530,7 +527,7 @@ public class MainViewModel extends AndroidViewModel { } public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) { - return db.addAccount(url, username, accountName, capabilities); + return repo.addAccount(url, username, accountName, capabilities); } public LiveData<Note> getFullNote$(long id) { @@ -539,7 +536,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 +549,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 +563,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 +572,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/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java index 4c307b07..dee5661f 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 @@ -63,7 +63,7 @@ public class ManageAccountsActivity extends LockedActivity { @Override public void onSuccess(Long unsynchronizedChangesCount) { runOnUiThread(() -> { - if (unsynchronizedChangesCount != null && unsynchronizedChangesCount > 0) { + 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)) 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 index 3e7e430a..2ee45cf8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java @@ -15,6 +15,7 @@ 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; @@ -25,28 +26,28 @@ public class ManageAccountsViewModel extends AndroidViewModel { private static final String TAG = ManageAccountsViewModel.class.getSimpleName(); @NonNull - private final NotesDatabase db; + private final NotesRepository repo; public ManageAccountsViewModel(@NonNull Application application) { super(application); - this.db = NotesDatabase.getInstance(application); + this.repo = NotesRepository.getInstance(application); } public void getCurrentAccount(@NonNull Context context, @NonNull IResponseCallback<Account> callback) { try { - callback.onSuccess(db.getAccountDao().getAccountByName((SingleAccountHelper.getCurrentSingleSignOnAccount(context).name))); + callback.onSuccess(repo.getAccountByName((SingleAccountHelper.getCurrentSingleSignOnAccount(context).name))); } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { callback.onError(e); } } public LiveData<List<Account>> getAccounts$() { - return distinctUntilChanged(db.getAccountDao().getAccounts$()); + return distinctUntilChanged(repo.getAccounts$()); } public void deleteAccount(@NonNull Account account, @NonNull Context context) { new Thread(() -> { - final List<Account> accounts = db.getAccountDao().getAccounts(); + final List<Account> accounts = repo.getAccounts(); for (int i = 0; i < accounts.size(); i++) { if (accounts.get(i).getId() == account.getId()) { if (i > 0) { @@ -56,7 +57,7 @@ public class ManageAccountsViewModel extends AndroidViewModel { } else { selectAccount(null, context); } - db.deleteAccount(accounts.get(i)); + repo.deleteAccount(accounts.get(i)); break; } } @@ -68,6 +69,6 @@ public class ManageAccountsViewModel extends AndroidViewModel { } public void countUnsynchronizedNotes(long accountId, @NonNull IResponseCallback<Long> callback) { - new Thread(() -> callback.onSuccess(db.getNoteDao().countUnsynchronizedNotes(accountId))).start(); + 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/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java index a9590225..8c38fbde 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,15 +42,15 @@ 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) { if (e instanceof NextcloudHttpRequestFailedException) { 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 c6f80280..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,404 +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 void deleteAccount(@NonNull Account localAccount) throws IllegalArgumentException { - validateAccountId(localAccount.getId()); - 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()); - }).start(); - } - - 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..55b786dc --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -0,0 +1,951 @@ +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.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.NavigationCategory; +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 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 = Executors.newSingleThreadExecutor(); + 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)) { + new Thread(() -> { + 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."); + } + }).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<>(); + + + public static synchronized NotesRepository getInstance(@NonNull Context context) { + if (instance == null) { + instance = new NotesRepository(context); + } + return instance; + } + + private NotesRepository(final Context context) { + this.context = context.getApplicationContext(); + this.db = NotesDatabase.getInstance(this.context); + 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)); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext()); + prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener); + syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false); + + updateNetworkStatus(); + } + + + // Accounts + + public List<Account> getAccounts() { + return db.getAccountDao().getAccounts(); + } + + public int deleteAccount(Account localAccount) { + return db.getAccountDao().deleteAccount(localAccount); + } + + 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); + } + + 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<>(); + new Thread(() -> ret.postValue(addNote(account.getId(), entity))).start(); + 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) { + validateAccountId(accountId); + return db.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(() -> { + db.getNoteDao().toggleFavorite(noteId); + 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(() -> { + db.getNoteDao().updateStatus(noteId, DBStatus.LOCAL_EDITED); + db.getNoteDao().updateCategory(noteId, category); + 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 = 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) { + new Thread(() -> { + 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."); + } + } + }).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 : 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); + } + } + } + }).start(); + } + + @AnyThread + public LiveData<Account> addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) { + return db.getAccountDao().getAccountById$(db.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 = db.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 = db.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 = db.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 (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(); + }).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 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); + } + + public Context getContext() { + return context; + } + + + @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, this, 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(); + } + } + 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) { + 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/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 dcc44979..2b4e6201 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 @@ -39,7 +39,7 @@ abstract class NotesServerSyncTask extends Thread { @NonNull private final NotesClient notesClient; @NonNull - private final NotesDatabase db; + private final NotesRepository repo; @NonNull protected final Account localAccount; @NonNull @@ -50,10 +50,10 @@ 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 NotesClient notesClient, @NonNull NotesRepository repo, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) { super(TAG); this.notesClient = notesClient; - this.db = db; + this.repo = repo; this.localAccount = localAccount; this.ssoAccount = ssoAccount; this.onlyLocalChanges = onlyLocalChanges; @@ -89,7 +89,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 { @@ -112,10 +112,10 @@ abstract class NotesServerSyncTask extends Thread { } 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()); + repo.updateRemoteId(note.getId(), remoteNote.getRemoteId()); } // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI. - db.getNoteDao().updateIfNotModifiedLocallyDuringSync(note.getId(), remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()), note.getContent(), note.getCategory(), note.getFavorite()); + 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) { @@ -133,7 +133,7 @@ abstract class NotesServerSyncTask extends Thread { } } // 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()); @@ -162,10 +162,10 @@ 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; @@ -186,14 +186,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)"); @@ -201,17 +201,17 @@ 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()); + repo.updateETag(localAccount.getId(), localAccount.getETag()); + repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis()); try { - if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) { + if (repo.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) { localAccount.setApiVersion(response.getSupportedApiVersions()); } } catch (Exception e) { 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/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java index 8cc73bf8..45979aa8 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; 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/widget/notelist/NoteListViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java index ad3830a4..9ec3e355 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListViewModel.java @@ -14,7 +14,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.main.MainActivity; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import static androidx.lifecycle.Transformations.distinctUntilChanged; import static androidx.lifecycle.Transformations.map; @@ -28,20 +28,20 @@ public class NoteListViewModel extends AndroidViewModel { private static final String TAG = NoteListViewModel.class.getSimpleName(); @NonNull - private final NotesDatabase db; + private final NotesRepository repo; public NoteListViewModel(@NonNull Application application) { super(application); - this.db = NotesDatabase.getInstance(application); + this.repo = NotesRepository.getInstance(application); } public LiveData<List<NavigationItem>> getAdapterCategories(Long accountId) { return distinctUntilChanged( - switchMap(distinctUntilChanged(db.getNoteDao().count$(accountId)), (count) -> { + switchMap(distinctUntilChanged(repo.count$(accountId)), (count) -> { Log.v(TAG, "[getAdapterCategories] countLiveData: " + count); - return switchMap(distinctUntilChanged(db.getNoteDao().countFavorites$(accountId)), (favoritesCount) -> { + return switchMap(distinctUntilChanged(repo.countFavorites$(accountId)), (favoritesCount) -> { Log.v(TAG, "[getAdapterCategories] getFavoritesCountLiveData: " + favoritesCount); - return map(distinctUntilChanged(db.getNoteDao().getCategories$(accountId)), fromDatabase -> { + return map(distinctUntilChanged(repo.getCategories$(accountId)), fromDatabase -> { final List<NavigationItem.CategoryNavigationItem> categories = convertToCategoryNavigationItem(getApplication(), fromDatabase); final List<NavigationItem> items = new ArrayList<>(fromDatabase.size() + 3); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java index 98ebf2fe..7cc84de3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java @@ -13,20 +13,20 @@ import android.widget.RemoteViews; import java.util.NoSuchElementException; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData; public class NoteListWidget extends AppWidgetProvider { private static final String TAG = NoteListWidget.class.getSimpleName(); static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { - final NotesDatabase db = NotesDatabase.getInstance(context); + final NotesRepository repo = NotesRepository.getInstance(context); RemoteViews views; for (int appWidgetId : appWidgetIds) { try { - final NotesListWidgetData data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId); + final NotesListWidgetData data = repo.getNoteListWidgetData(appWidgetId); final Intent serviceIntent = new Intent(context, NoteListWidgetService.class); serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); @@ -80,10 +80,10 @@ public class NoteListWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); - final NotesDatabase db = NotesDatabase.getInstance(context); + final NotesRepository repo = NotesRepository.getInstance(context); for (int appWidgetId : appWidgetIds) { - new Thread(() -> db.getWidgetNotesListDao().removeNoteListWidget(appWidgetId)).start(); + new Thread(() -> repo.removeNoteListWidget(appWidgetId)).start(); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java index 2c2aa086..a750ee1e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java @@ -21,11 +21,9 @@ import it.niedermann.owncloud.notes.databinding.ActivityNoteListConfigurationBin import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; -import it.niedermann.owncloud.notes.persistence.entity.CategoryOptions; import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData; -import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_ALL; import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_CATEGORY; @@ -43,14 +41,14 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { private ActivityNoteListConfigurationBinding binding; private NoteListViewModel viewModel; private NavigationAdapter adapterCategories; - private NotesDatabase db = null; + private NotesRepository repo = null; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setResult(RESULT_CANCELED); - db = NotesDatabase.getInstance(this); + repo = NotesRepository.getInstance(this); final Bundle extras = getIntent().getExtras(); if (extras != null) { @@ -107,7 +105,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId()); new Thread(() -> { - db.getWidgetNotesListDao().createOrUpdateNoteListWidgetData(data); + repo.createOrUpdateNoteListWidgetData(data); final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), NoteListWidget.class) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); @@ -126,7 +124,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { new Thread(() -> { try { - this.localAccount = db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name); + this.localAccount = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name); } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { e.printStackTrace(); Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java index 7917b25a..b7482233 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java @@ -19,7 +19,7 @@ import java.util.List; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.main.MainActivity; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData; @@ -37,7 +37,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact private final Context context; private final int appWidgetId; - private final NotesDatabase db; + private final NotesRepository repo; @NonNull private final List<Note> dbNotes = new ArrayList<>(); private NotesListWidgetData data; @@ -45,7 +45,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact NoteListWidgetFactory(Context context, Intent intent) { this.context = context; this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - db = NotesDatabase.getInstance(context); + repo = NotesRepository.getInstance(context); } @Override @@ -57,21 +57,21 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact public void onDataSetChanged() { dbNotes.clear(); try { - data = db.getWidgetNotesListDao().getNoteListWidgetData(appWidgetId); + data = repo.getNoteListWidgetData(appWidgetId); Log.v(TAG, "--- data - " + data); switch (data.getMode()) { case MODE_DISPLAY_ALL: - dbNotes.addAll(db.getNoteDao().searchRecentByModified(data.getAccountId(), "%")); + dbNotes.addAll(repo.searchRecentByModified(data.getAccountId(), "%")); break; case MODE_DISPLAY_STARRED: - dbNotes.addAll(db.getNoteDao().searchFavoritesByModified(data.getAccountId(), "%")); + dbNotes.addAll(repo.searchFavoritesByModified(data.getAccountId(), "%")); break; case MODE_DISPLAY_CATEGORY: default: if (data.getCategory() != null) { - dbNotes.addAll(db.getNoteDao().searchCategoryByModified(data.getAccountId(), "%", data.getCategory())); + dbNotes.addAll(repo.searchCategoryByModified(data.getAccountId(), "%", data.getCategory())); } else { - dbNotes.addAll(db.getNoteDao().searchUncategorizedByModified(data.getAccountId(), "%")); + dbNotes.addAll(repo.searchUncategorizedByModified(data.getAccountId(), "%")); } break; } @@ -95,7 +95,7 @@ public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFact final RemoteViews note_content; if (position == 0) { - final Account localAccount = db.getAccountDao().getAccountById(data.getAccountId()); + final Account localAccount = repo.getAccountById(data.getAccountId()); final Intent openIntent = new Intent(Intent.ACTION_MAIN).setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName())); final Intent createIntent = new Intent(context, EditNoteActivity.class); final Bundle extras = new Bundle(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java index 3ad56c76..e208603a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java @@ -11,9 +11,9 @@ import android.util.Log; import android.widget.RemoteViews; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.edit.BaseNoteFragment; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData; public class SingleNoteWidget extends AppWidgetProvider { @@ -22,11 +22,11 @@ public class SingleNoteWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { final Intent templateIntent = new Intent(context, EditNoteActivity.class); - final NotesDatabase db = NotesDatabase.getInstance(context); + final NotesRepository repo = NotesRepository.getInstance(context); for (int appWidgetId : appWidgetIds) { - final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId); - if(data != null) { + final SingleNoteWidgetData data = repo.getSingleNoteWidgetData(appWidgetId); + if (data != null) { templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, data.getAccountId()); final PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent, @@ -66,10 +66,10 @@ public class SingleNoteWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { - final NotesDatabase db = NotesDatabase.getInstance(context); + final NotesRepository repo = NotesRepository.getInstance(context); for (int appWidgetId : appWidgetIds) { - new Thread(() -> db.getWidgetSingleNoteDao().removeSingleNoteWidget(appWidgetId)).start(); + new Thread(() -> repo.removeSingleNoteWidget(appWidgetId)).start(); } super.onDeleted(context, appWidgetIds); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java index db26da24..f6ca4a24 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java @@ -13,7 +13,7 @@ import androidx.annotation.Nullable; import it.niedermann.android.markdown.MarkdownUtil; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.edit.EditNoteActivity; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData; @@ -22,7 +22,7 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa private final Context context; private final int appWidgetId; - private final NotesDatabase db; + private final NotesRepository repo; @Nullable private Note note; @@ -31,7 +31,7 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa SingleNoteWidgetFactory(Context context, Intent intent) { this.context = context; this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - this.db = NotesDatabase.getInstance(context); + this.repo = NotesRepository.getInstance(context); } @Override @@ -41,11 +41,11 @@ public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFa @Override public void onDataSetChanged() { - final SingleNoteWidgetData data = db.getWidgetSingleNoteDao().getSingleNoteWidgetData(appWidgetId); - if(data != null) { + final SingleNoteWidgetData data = repo.getSingleNoteWidgetData(appWidgetId); + if (data != null) { final long noteId = data.getNoteId(); Log.v(TAG, "Fetch note with id " + noteId); - note = db.getNoteDao().getNoteById(noteId); + note = repo.getNoteById(noteId); if (note == null) { Log.e(TAG, "Error: note not found"); diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java new file mode 100644 index 00000000..7859b2eb --- /dev/null +++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java @@ -0,0 +1,151 @@ +package it.niedermann.owncloud.notes.persistence; + +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.arch.core.executor.testing.InstantTaskExecutorRule; +import androidx.room.Room; +import androidx.test.core.app.ApplicationProvider; + +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Map; + +import it.niedermann.owncloud.notes.persistence.entity.Account; +import it.niedermann.owncloud.notes.persistence.entity.Note; +import it.niedermann.owncloud.notes.shared.model.Capabilities; + +import static it.niedermann.owncloud.notes.persistence.NotesDatabaseTestUtil.getOrAwaitValue; +import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED; +import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED; +import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = {Build.VERSION_CODES.P}) +public class NotesRepositoryTest { + + @Rule + public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); + + @NonNull + private Context context; + private NotesRepository repoMock = null; + private Account account = null; + private Account secondAccount = null; + + @Before + public void setupDB() throws NextcloudHttpRequestFailedException { + context = ApplicationProvider.getApplicationContext(); + repoMock = Mockito.mock(NotesRepository.class); + final NotesDatabase db = Room + .inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class) + .allowMainThreadQueries() + .build(); + Mockito.when(repoMock.db.getNoteDao()).thenReturn(db.getNoteDao()); + + repoMock.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":200,\"message\":\"OK\"},\"data\":{\"version\":{\"major\":18,\"minor\":0,\"micro\":4,\"string\":\"18.0.4\",\"edition\":\"\",\"extendedSupport\":false},\"capabilities\":{\"core\":{\"pollinterval\":60,\"webdav-root\":\"remote.php\\/webdav\"},\"bruteforce\":{\"delay\":0},\"files\":{\"bigfilechunking\":true,\"blacklisted_files\":[\".htaccess\"],\"directEditing\":{\"url\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/files\\/api\\/v1\\/directEditing\",\"etag\":\"ed2b141af2a39b0e42666952ba60988d\"},\"versioning\":true,\"undelete\":true},\"activity\":{\"apiv2\":[\"filters\",\"filters-api\",\"previews\",\"rich-strings\"]},\"ocm\":{\"enabled\":true,\"apiVersion\":\"1.0-proposal1\",\"endPoint\":\"https:\\/\\/efss.qloud.my\\/index.php\\/ocm\",\"resourceTypes\":[{\"name\":\"file\",\"shareTypes\":[\"user\",\"group\"],\"protocols\":{\"webdav\":\"\\/public.php\\/webdav\\/\"}}]},\"deck\":{\"version\":\"0.8.2\"},\"richdocuments\":{\"mimetypes\":[\"application\\/vnd.oasis.opendocument.text\",\"application\\/vnd.oasis.opendocument.spreadsheet\",\"application\\/vnd.oasis.opendocument.graphics\",\"application\\/vnd.oasis.opendocument.presentation\",\"application\\/vnd.lotus-wordpro\",\"application\\/vnd.visio\",\"application\\/vnd.wordperfect\",\"application\\/msonenote\",\"application\\/msword\",\"application\\/rtf\",\"text\\/rtf\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.document\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.template\",\"application\\/vnd.ms-word.document.macroEnabled.12\",\"application\\/vnd.ms-word.template.macroEnabled.12\",\"application\\/vnd.ms-excel\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.template\",\"application\\/vnd.ms-excel.sheet.macroEnabled.12\",\"application\\/vnd.ms-excel.template.macroEnabled.12\",\"application\\/vnd.ms-excel.addin.macroEnabled.12\",\"application\\/vnd.ms-excel.sheet.binary.macroEnabled.12\",\"application\\/vnd.ms-powerpoint\",\"application\\/vnd.openxmlformats-officedocument.presentationml.presentation\",\"application\\/vnd.openxmlformats-officedocument.presentationml.template\",\"application\\/vnd.openxmlformats-officedocument.presentationml.slideshow\",\"application\\/vnd.ms-powerpoint.addin.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.presentation.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.template.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.slideshow.macroEnabled.12\",\"text\\/csv\"],\"mimetypesNoDefaultOpen\":[\"image\\/svg+xml\",\"application\\/pdf\",\"text\\/plain\",\"text\\/spreadsheet\"],\"collabora\":[],\"direct_editing\":false,\"templates\":false,\"productName\":\"\\u5728\\u7ebf\\u534f\\u4f5c\"},\"dav\":{\"chunking\":\"1.0\"},\"files_sharing\":{\"api_enabled\":true,\"public\":{\"enabled\":true,\"password\":{\"enforced\":true,\"askForOptionalPassword\":false},\"expire_date\":{\"enabled\":true,\"days\":\"7\",\"enforced\":false},\"multiple_links\":true,\"expire_date_internal\":{\"enabled\":false},\"send_mail\":false,\"upload\":true,\"upload_files_drop\":true},\"resharing\":true,\"user\":{\"send_mail\":false,\"expire_date\":{\"enabled\":true}},\"group_sharing\":true,\"group\":{\"enabled\":true,\"expire_date\":{\"enabled\":true}},\"default_permissions\":31,\"federation\":{\"outgoing\":false,\"incoming\":false,\"expire_date\":{\"enabled\":true}},\"sharee\":{\"query_lookup_default\":false},\"sharebymail\":{\"enabled\":true,\"upload_files_drop\":{\"enabled\":true},\"password\":{\"enabled\":true},\"expire_date\":{\"enabled\":true}}},\"external\":{\"v1\":[\"sites\",\"device\",\"groups\",\"redirect\"]},\"notifications\":{\"ocs-endpoints\":[\"list\",\"get\",\"delete\",\"delete-all\",\"icons\",\"rich-strings\",\"action-web\"],\"push\":[\"devices\",\"object-data\",\"delete\"],\"admin-notifications\":[\"ocs\",\"cli\"]},\"password_policy\":{\"minLength\":8,\"enforceNonCommonPassword\":true,\"enforceNumericCharacters\":false,\"enforceSpecialCharacters\":false,\"enforceUpperLowerCase\":false,\"api\":{\"generate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/generate\",\"validate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/validate\"}},\"theming\":{\"name\":\"QloudData\",\"url\":\"https:\\/\\/www.qloud.my\\/qloud-data\\/\",\"slogan\":\"Powered by NextCloud\",\"color\":\"#1E4164\",\"color-text\":\"#ffffff\",\"color-element\":\"#1E4164\",\"logo\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"background\":\"https:\\/\\/efss.qloud.my\\/core\\/img\\/background.png?v=47\",\"background-plain\":false,\"background-default\":true,\"logoheader\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"favicon\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\"},\"registration\":{\"enabled\":true,\"apiRoot\":\"\\/ocs\\/v2.php\\/apps\\/registration\\/api\\/v1\\/\",\"apiLevel\":\"v1\"}}}}}", null)); + account = repoMock.getAccountDao().getAccountByName("彼得@äöüß.example.com"); + + repoMock.addAccount("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null)); + secondAccount = repoMock.getAccountDao().getAccountByName("test@example.org"); + + Arrays.stream(new Note[]{ + new Note(1, 1001L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, VOID, account.getId(), "", 0), + new Note(2, null, Calendar.getInstance(), "T", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0), + new Note(3, 1003L, Calendar.getInstance(), "美好的一天", "C", "Movies", false, null, LOCAL_EDITED, account.getId(), "", 0), + new Note(4, null, Calendar.getInstance(), "T", "C", "Music", false, null, VOID, account.getId(), "", 0), + new Note(5, 1005L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_EDITED, account.getId(), "", 0), + new Note(6, 1006L, Calendar.getInstance(), "美好的一天", "C", " 兄弟,这真是美好的一天。", false, null, LOCAL_DELETED, account.getId(), "", 0), + new Note(7, null, Calendar.getInstance(), "T", "C", "Music", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0), + new Note(8, 1008L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_EDITED, secondAccount.getId(), "", 0), + new Note(9, 1009L, Calendar.getInstance(), "美好的一天", "C", "ToDo", true, null, LOCAL_DELETED, secondAccount.getId(), "", 0) + }).forEach(note -> repoMock.getNoteDao().addNote(note)); + } + + @After + public void closeDb() { + repoMock.close(); + } + + @Test + public void testGetIdMap() { + final Map<Long, Long> idMapOfFirstAccount = repoMock.getIdMap(account.getId()); + assertEquals(3, idMapOfFirstAccount.size()); + assertEquals(Long.valueOf(1L), idMapOfFirstAccount.get(1001L)); + assertEquals(Long.valueOf(3L), idMapOfFirstAccount.get(1003L)); + assertEquals(Long.valueOf(5L), idMapOfFirstAccount.get(1005L)); + + final Map<Long, Long> idMapOfSecondAccount = repoMock.getIdMap(secondAccount.getId()); + assertEquals(1, idMapOfSecondAccount.size()); + assertEquals(Long.valueOf(8L), idMapOfSecondAccount.get(1008L)); + } + + @Test + public void testAddAccount() throws NextcloudHttpRequestFailedException, InterruptedException { + final Account createdAccount = getOrAwaitValue(repoMock.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null))); + assertEquals("https://äöüß.example.com", createdAccount.getUrl()); + assertEquals("彼得", createdAccount.getUserName()); + assertEquals("彼得@äöüß.example.com", createdAccount.getAccountName()); + } + + @Test + public void testAddNote() { + final Note localNote = new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123"); + localNote.setId(99); + final Note createdNoteFromLocal = repoMock.addNote(account.getId(), localNote); + assertEquals(LOCAL_EDITED, createdNoteFromLocal.getStatus()); + assertEquals("MyContent", createdNoteFromLocal.getExcerpt()); + + final Note createdNoteFromRemote = repoMock.addNote(account.getId(), new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123")); + assertEquals(VOID, createdNoteFromRemote.getStatus()); + assertEquals("MyContent", createdNoteFromRemote.getExcerpt()); + } + + @Test + public void updateApiVersion() { + assertThrows(IllegalArgumentException.class, () -> repoMock.updateApiVersion(account.getId(), "")); + assertThrows(IllegalArgumentException.class, () -> repoMock.updateApiVersion(account.getId(), "asdf")); + assertThrows(IllegalArgumentException.class, () -> repoMock.updateApiVersion(account.getId(), "{}")); + + repoMock.updateApiVersion(account.getId(), null); + assertNull(repoMock.getAccountDao().getAccountById(account.getId()).getApiVersion()); + repoMock.updateApiVersion(account.getId(), "[]"); + assertNull(repoMock.getAccountDao().getAccountById(account.getId()).getApiVersion()); + + repoMock.updateApiVersion(account.getId(), "[1.0]"); + assertEquals("[1.0]", repoMock.getAccountDao().getAccountById(account.getId()).getApiVersion()); + repoMock.updateApiVersion(account.getId(), "[0.2, 1.0]"); + assertEquals("[0.2, 1.0]", repoMock.getAccountDao().getAccountById(account.getId()).getApiVersion()); + + // TODO is this really indented? + repoMock.updateApiVersion(account.getId(), "[0.2, abc]"); + assertEquals("[0.2, abc]", repoMock.getAccountDao().getAccountById(account.getId()).getApiVersion()); + } + + @Test + @Ignore("Need to find a way to stub deleteAndSync method") + public void moveNoteToAnotherAccount() throws InterruptedException { + final Note noteToMove = repoMock.getNoteDao().getNoteById(1); + assertEquals(3, repoMock.getNoteDao().getLocalModifiedNotes(secondAccount.getId()).size()); + final Note movedNote = getOrAwaitValue(repoMock.moveNoteToAnotherAccount(secondAccount, noteToMove)); + assertEquals(4, repoMock.getNoteDao().getLocalModifiedNotes(secondAccount.getId()).size()); + assertEquals(LOCAL_EDITED, movedNote.getStatus()); + // TODO assert deleteAndSync has been called + } +}
\ No newline at end of file |