diff options
author | Stefan Niedermann <info@niedermann.it> | 2021-04-20 10:47:53 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2021-04-20 10:47:53 +0300 |
commit | fa03e56883e93d3181c48c5eb9dc1d6c325469ad (patch) | |
tree | 6ad0234043055b8810d8d4dcb0175ea1e9699d23 | |
parent | e8e350e99cb322ad0dc3da2eb9493ae7ed21e2a7 (diff) |
#831 Better handling of error states while synchronizing capabilities and notes by using callbacks instead of LiveData (which is not really error aware)
4 files changed, 149 insertions, 86 deletions
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java new file mode 100644 index 00000000..1cd1f206 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java @@ -0,0 +1,14 @@ +package it.niedermann.owncloud.notes.exception; + +import androidx.annotation.NonNull; + +/** + * This type of {@link Exception} occurs, when a user has an active internet connection but decided by intention not to use it. + * Example: "Sync only on Wi-Fi" is set to <code>true</code>, Wi-Fi is not connected, mobile data is available + */ +public class IntendedOfflineException extends Exception { + + public IntendedOfflineException(@NonNull String message) { + super(message); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index f36c0c02..3600b0d6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -58,6 +58,7 @@ import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; import it.niedermann.owncloud.notes.edit.category.CategoryViewModel; import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; +import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; import it.niedermann.owncloud.notes.main.items.ItemAdapter; import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; @@ -74,6 +75,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; +import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.model.NoteClickListener; import it.niedermann.owncloud.notes.shared.util.NoteUtil; @@ -265,11 +267,21 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A .apply(RequestOptions.circleCropTransform()) .into(activityBinding.launchAccountSwitcher); - final LiveData<Boolean> syncLiveData = mainViewModel.synchronize(); - syncLiveData.observe(this, (syncSuccess) -> { - syncLiveData.removeObservers(this); - if (!syncSuccess) { - BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); + mainViewModel.synchronizeNotes(nextAccount, new IResponseCallback() { + @Override + public void onSuccess() { + Log.d(TAG, "Successfully synchronized notes for " + nextAccount.getAccountName()); + } + + @Override + public void onError(@NonNull Throwable t) { + runOnUiThread(() -> { + if (t.getClass() == IntendedOfflineException.class || t instanceof IntendedOfflineException) { + Log.i(TAG, "Capabilities and notes not updated because " + nextAccount.getAccountName() + " is offline by intention."); + } else { + BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); + } + }); } }); fabCreate.show(); @@ -294,12 +306,20 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A @Override protected void onResume() { - final LiveData<Boolean> syncLiveData = mainViewModel.synchronize(); - syncLiveData.observe(this, (syncSuccess) -> { - syncLiveData.removeObservers(this); - if (!syncSuccess) { - BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); - } + final LiveData<Account> accountLiveData = mainViewModel.getCurrentAccount(); + accountLiveData.observe(this, (currentAccount) -> { + accountLiveData.removeObservers(this); + mainViewModel.synchronizeNotes(currentAccount, new IResponseCallback() { + @Override + public void onSuccess() { + Log.d(TAG, "Successfully synchronized notes for " + currentAccount.getAccountName()); + } + + @Override + public void onError(@NonNull Throwable t) { + t.printStackTrace(); + } + }); }); super.onResume(); } @@ -404,13 +424,28 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A new Thread(() -> { Log.i(TAG, "Clearing Glide disk cache"); Glide.get(getApplicationContext()).clearDiskCache(); - }).start(); - final LiveData<Boolean> syncLiveData = mainViewModel.performFullSynchronizationForCurrentAccount(); - final Observer<Boolean> syncObserver = syncSuccess -> { - if (!syncSuccess) { - BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); - } + }, "CLEAR_GLIDE_CACHE").start(); + final LiveData<Account> syncLiveData = mainViewModel.getCurrentAccount(); + final Observer<Account> syncObserver = currentAccount -> { syncLiveData.removeObservers(this); + mainViewModel.synchronizeCapabilitiesAndNotes(currentAccount, new IResponseCallback() { + @Override + public void onSuccess() { + Log.d(TAG, "Successfully synchronized capabilities and notes for " + currentAccount.getAccountName()); + } + + @Override + public void onError(@NonNull Throwable t) { + runOnUiThread(() -> { + swipeRefreshLayout.setRefreshing(false); + if (t.getClass() == IntendedOfflineException.class || t instanceof IntendedOfflineException) { + Log.i(TAG, "Capabilities and notes not updated because " + currentAccount.getAccountName() + " is offline by intention."); + } else { + BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); + } + }); + } + }); }; syncLiveData.observe(this, syncObserver); }); 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 b528941c..0241951e 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 @@ -1,5 +1,6 @@ package it.niedermann.owncloud.notes.main; +import android.accounts.NetworkErrorException; import android.app.Application; import android.content.Context; import android.text.TextUtils; @@ -29,6 +30,7 @@ import java.util.stream.Collectors; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; @@ -40,6 +42,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; +import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.Item; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; @@ -366,32 +369,83 @@ public class MainViewModel extends AndroidViewModel { return items; } + public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback callback) { + Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize capabilities for " + localAccount.getAccountName()); + synchronizeCapabilities(localAccount, new IResponseCallback() { + @Override + public void onSuccess() { + Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize notes for " + localAccount.getAccountName()); + synchronizeNotes(localAccount, callback); + } + + @Override + public void onError(@NonNull Throwable t) { + callback.onError(t); + } + }); + } + /** - * @return <code>true</code>, if a synchronization could successfully be triggered, <code>false</code> if not. + * Updates the network status if necessary and pulls the latest {@link Capabilities} of the given {@param localAccount} */ - public LiveData<Boolean> synchronize() { - return switchMap(getCurrentAccount(), currentAccount -> { - if (currentAccount == null) { - return new MutableLiveData<>(false); - } else { - Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName()); - NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper(); - if (!syncHelper.isSyncPossible()) { - syncHelper.updateNetworkStatus(); - } - if (syncHelper.isSyncPossible()) { - syncHelper.scheduleSync(currentAccount, false); - return new MutableLiveData<>(true); - } else { // Sync is not possible - if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) { - Log.d(TAG, "Network is connected, but sync is not possible"); + public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback callback) { + new Thread(() -> { + final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper(); + if (!syncHelper.isSyncPossible()) { + syncHelper.updateNetworkStatus(); + } + if (syncHelper.isSyncPossible()) { + try { + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag()); + db.getAccountDao().updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); + db.getAccountDao().updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor()); + localAccount.setColor(capabilities.getColor()); + localAccount.setTextColor(capabilities.getTextColor()); + BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor()); + db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); + callback.onSuccess(); + } catch (NextcloudFilesAppAccountNotFoundException e) { + db.getAccountDao().deleteAccount(localAccount); + callback.onError(e); + } catch (Exception e) { + if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + Log.i(TAG, "[synchronizeCapabilities] Capabilities not modified."); + callback.onSuccess(); } else { - Log.d(TAG, "Sync is not possible, because network is not connected"); + callback.onError(e); } } - return new MutableLiveData<>(false); + } else { + if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) { + callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible.")); + } else { + callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected.")); + } } - }); + }, "SYNC_CAPABILITIES").start(); + } + + /** + * Updates the network status if necessary and pulls the latest notes of the given {@param localAccount} + */ + public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback callback) { + new Thread(() -> { + Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName()); + final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper(); + if (!syncHelper.isSyncPossible()) { + syncHelper.updateNetworkStatus(); + } + if (syncHelper.isSyncPossible()) { + syncHelper.scheduleSync(currentAccount, false); + callback.onSuccess(); + } else { // Sync is not possible + if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) { + callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible.")); + } else { + callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected.")); + } + } + }, "SYNC_NOTES").start(); } public LiveData<Boolean> getSyncStatus() { @@ -406,55 +460,6 @@ public class MainViewModel extends AndroidViewModel { return map(db.getAccountDao().countAccounts$(), (counter) -> counter != null && counter > 1); } - public LiveData<Boolean> performFullSynchronizationForCurrentAccount() { - final MutableLiveData<Boolean> insufficientInformation = new MutableLiveData<>(); - return switchMap(getCurrentAccount(), localAccount -> { - Log.v(TAG, "[performFullSynchronizationForCurrentAccount] - currentAccount: " + localAccount); - if (localAccount == null) { - return insufficientInformation; - } else { - Log.i(TAG, "[performFullSynchronizationForCurrentAccount] Refreshing capabilities for " + localAccount.getAccountName()); - final MutableLiveData<Boolean> syncCapabilitiesLiveData = new MutableLiveData<>(); - new Thread(() -> { - final Capabilities capabilities; - try { - capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag()); - db.getAccountDao().updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); - db.getAccountDao().updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor()); - localAccount.setColor(capabilities.getColor()); - localAccount.setTextColor(capabilities.getTextColor()); - BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor()); - db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); - Log.i(TAG, capabilities.toString()); - syncCapabilitiesLiveData.postValue(true); - } catch (NextcloudFilesAppAccountNotFoundException e) { - e.printStackTrace(); - db.getAccountDao().deleteAccount(localAccount); - syncCapabilitiesLiveData.postValue(false); - } catch (Exception e) { - if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { - Log.i(TAG, "[performFullSynchronizationForCurrentAccount] Capabilities not modified."); - } else { - e.printStackTrace(); - } - // Capabilities couldn't be update correctly, we can still try to sync the notes list. - syncCapabilitiesLiveData.postValue(true); - } - - }).start(); - return switchMap(syncCapabilitiesLiveData, capabilitiesSyncedSuccessfully -> { - if (Boolean.TRUE.equals(capabilitiesSyncedSuccessfully)) { - Log.v(TAG, "[performFullSynchronizationForCurrentAccount] Capabilities refreshed successfully - synchronize notes for " + localAccount.getAccountName()); - return synchronize(); - } else { - Log.w(TAG, "[performFullSynchronizationForCurrentAccount] Capabilities could not be refreshed correctly - end synchronization process here."); - return new MutableLiveData<>(true); - } - }); - } - }); - } - @WorkerThread public Account getLocalAccountByAccountName(String accountName) { return db.getAccountDao().getAccountByName(accountName); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java new file mode 100644 index 00000000..2c329727 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/IResponseCallback.java @@ -0,0 +1,9 @@ +package it.niedermann.owncloud.notes.shared.model; + +import androidx.annotation.NonNull; + +public interface IResponseCallback { + void onSuccess(); + + void onError(@NonNull Throwable t); +} |