Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa03e568 authored by Stefan Niedermann's avatar Stefan Niedermann
Browse files

#831 Better handling of error states while synchronizing capabilities and...

#831 Better handling of error states while synchronizing capabilities and notes by using callbacks instead of LiveData (which is not really error aware)
parent e8e350e9
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
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);
    }
}
+52 −17
Original line number Diff line number Diff line
@@ -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,13 +267,23 @@ 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) {
            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();
            activityBinding.launchAccountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance(nextAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));

@@ -294,13 +306,21 @@ 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) {
            }, "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.removeObservers(this);
                        });
                    }
                });
            };
            syncLiveData.observe(this, syncObserver);
        });
+74 −69
Original line number Diff line number Diff line
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);
    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());
                NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
            final NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
            if (!syncHelper.isSyncPossible()) {
                syncHelper.updateNetworkStatus();
            }
            if (syncHelper.isSyncPossible()) {
                syncHelper.scheduleSync(currentAccount, false);
                    return new MutableLiveData<>(true);
                callback.onSuccess();
            } else { // Sync is not possible
                if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) {
                        Log.d(TAG, "Network is connected, but sync is not possible");
                    callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
                } else {
                        Log.d(TAG, "Sync is not possible, because network is not connected");
                    callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
                }
            }
                return new MutableLiveData<>(false);
            }
        });
        }, "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);
+9 −0
Original line number Diff line number Diff line
package it.niedermann.owncloud.notes.shared.model;

import androidx.annotation.NonNull;

public interface IResponseCallback {
    void onSuccess();

    void onError(@NonNull Throwable t);
}