#831 Better handling of error states while synchronizing capabilities and notes by using callbacks instead of LiveData (which is not really error aware)

This commit is contained in:
Stefan Niedermann 2021-04-20 09:47:53 +02:00
parent e8e350e99c
commit fa03e56883
4 changed files with 153 additions and 90 deletions

View file

@ -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);
}
}

View file

@ -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);
});

View file

@ -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,34 +369,85 @@ public class MainViewModel extends AndroidViewModel {
return items;
}
/**
* @return <code>true</code>, if a synchronization could successfully be triggered, <code>false</code> if not.
*/
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");
} else {
Log.d(TAG, "Sync is not possible, because network is not connected");
}
}
return new MutableLiveData<>(false);
public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize capabilities for " + localAccount.getAccountName());
synchronizeCapabilities(localAccount, new IResponseCallback() {
@Override
public void onSuccess() {
Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize notes for " + localAccount.getAccountName());
synchronizeNotes(localAccount, callback);
}
@Override
public void onError(@NonNull Throwable t) {
callback.onError(t);
}
});
}
/**
* Updates the network status if necessary and pulls the latest {@link Capabilities} of the given {@param localAccount}
*/
public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
new Thread(() -> {
final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
if (!syncHelper.isSyncPossible()) {
syncHelper.updateNetworkStatus();
}
if (syncHelper.isSyncPossible()) {
try {
final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag());
db.getAccountDao().updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
db.getAccountDao().updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor());
localAccount.setColor(capabilities.getColor());
localAccount.setTextColor(capabilities.getTextColor());
BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor());
db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
callback.onSuccess();
} catch (NextcloudFilesAppAccountNotFoundException e) {
db.getAccountDao().deleteAccount(localAccount);
callback.onError(e);
} catch (Exception e) {
if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.i(TAG, "[synchronizeCapabilities] Capabilities not modified.");
callback.onSuccess();
} else {
callback.onError(e);
}
}
} else {
if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
} else {
callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
}
}
}, "SYNC_CAPABILITIES").start();
}
/**
* Updates the network status if necessary and pulls the latest notes of the given {@param localAccount}
*/
public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback callback) {
new Thread(() -> {
Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
if (!syncHelper.isSyncPossible()) {
syncHelper.updateNetworkStatus();
}
if (syncHelper.isSyncPossible()) {
syncHelper.scheduleSync(currentAccount, false);
callback.onSuccess();
} else { // Sync is not possible
if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
} else {
callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
}
}
}, "SYNC_NOTES").start();
}
public LiveData<Boolean> getSyncStatus() {
return db.getNoteServerSyncHelper().getSyncStatus();
}
@ -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);

View file

@ -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);
}