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

Commit 972a3861 authored by Stefan Niedermann's avatar Stefan Niedermann
Browse files

#1167 Cache Retrofit APIs

They use reflection and should therefore be cached.
parent 1768b2b4
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ import it.niedermann.owncloud.notes.databinding.ActivityImportAccountBinding;
import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
import it.niedermann.owncloud.notes.exception.ExceptionHandler;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.SSOClient;
import it.niedermann.owncloud.notes.persistence.ApiProvider;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
@@ -110,7 +110,7 @@ public class ImportAccountActivity extends AppCompatActivity {
                        });
                    } catch (Throwable t) {
                        t.printStackTrace();
                        SSOClient.invalidateAPICache(ssoAccount);
                        ApiProvider.invalidateAPICache(ssoAccount);
                        SingleAccountHelper.setCurrentAccount(this, null);
                        runOnUiThread(() -> {
                            restoreCleanState();
+2 −2
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker;
import it.niedermann.owncloud.notes.persistence.SSOClient;
import it.niedermann.owncloud.notes.persistence.ApiProvider;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@@ -673,7 +673,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
                                    }
                                });
                            } catch (Throwable e) {
                                SSOClient.invalidateAPICache(ssoAccount);
                                ApiProvider.invalidateAPICache(ssoAccount);
                                // Happens when importing an already existing account the second time
                                if (e instanceof TokenMismatchException && mainViewModel.getLocalAccountByAccountName(ssoAccount.name) != null) {
                                    Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported.");
+137 −0
Original line number Diff line number Diff line
@@ -4,8 +4,10 @@ import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonPrimitive;
@@ -18,26 +20,63 @@ import java.util.HashMap;
import java.util.Map;

import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer;
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
import retrofit2.NextcloudRetrofitApiBuilder;
import retrofit2.Retrofit;

@SuppressWarnings("WeakerAccess")
/**
 * Since creating APIs via {@link Retrofit} uses reflection and {@link NextcloudAPI} <a href="https://github.com/nextcloud/Android-SingleSignOn/issues/120#issuecomment-540069990">is supposed to stay alive as long as possible</a>, those artifacts are going to be cached.
 * They can be invalidated by using either {@link #invalidateAPICache()} for all or {@link #invalidateAPICache(SingleSignOnAccount)} for a specific {@link SingleSignOnAccount} and will be recreated when they are queried the next time.
 */
@WorkerThread
public class SSOClient {
public class ApiProvider {

    private static final String TAG = ApiProvider.class.getSimpleName();

    private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";

    private static final String TAG = SSOClient.class.getSimpleName();
    private static final Map<String, NextcloudAPI> API_CACHE = new HashMap<>();

    private static final Map<String, OcsAPI> API_CACHE_OCS = new HashMap<>();
    private static final Map<String, NotesAPI> API_CACHE_NOTES = new HashMap<>();

    /**
     * An {@link OcsAPI} currently shares the {@link Gson} configuration with the {@link NotesAPI} and therefore divides all {@link Calendar} milliseconds by 1000 while serializing and multiplies values by 1000 during deserialization.
     */
    public static synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
        if (API_CACHE_OCS.containsKey(ssoAccount.name)) {
            return API_CACHE_OCS.get(ssoAccount.name);
        }
        final OcsAPI ocsAPI = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_OCS).create(OcsAPI.class);
        API_CACHE_OCS.put(ssoAccount.name, ocsAPI);
        return ocsAPI;
    }

    private static final Map<String, NextcloudAPI> mNextcloudAPIs = new HashMap<>();
    /**
     * In case the {@param preferredApiVersion} changes, call {@link #invalidateAPICache(SingleSignOnAccount)} or {@link #invalidateAPICache()} to make sure that this call returns a {@link NotesAPI} that uses the correct compatibility layer.
     */
    public static synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) {
        if (API_CACHE_NOTES.containsKey(ssoAccount.name)) {
            return API_CACHE_NOTES.get(ssoAccount.name);
        }
        final NotesAPI notesAPI = new NotesAPI(getNextcloudAPI(context, ssoAccount), preferredApiVersion);
        API_CACHE_NOTES.put(ssoAccount.name, notesAPI);
        return notesAPI;
    }

    public static synchronized NextcloudAPI getNextcloudAPI(@NonNull Context appContext, @NonNull SingleSignOnAccount ssoAccount) {
        if (mNextcloudAPIs.containsKey(ssoAccount.name)) {
            return mNextcloudAPIs.get(ssoAccount.name);
    private static synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
        if (API_CACHE.containsKey(ssoAccount.name)) {
            return API_CACHE.get(ssoAccount.name);
        } else {
            Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
            final NextcloudAPI nextcloudAPI = new NextcloudAPI(appContext, ssoAccount,
            final NextcloudAPI nextcloudAPI = new NextcloudAPI(context.getApplicationContext(), ssoAccount,
                    new GsonBuilder()
                            .excludeFieldsWithoutExposeAnnotation()
                            .registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer<Calendar>) (src, typeOfSrc, context) -> new JsonPrimitive(src.getTimeInMillis() / 1_000))
                            .registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer<Calendar>) (src, typeOfSrc, context) -> {
                            .registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer<Calendar>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.getTimeInMillis() / 1_000))
                            .registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer<Calendar>) (src, typeOfSrc, ctx) -> {
                                final Calendar calendar = Calendar.getInstance();
                                calendar.setTimeInMillis(src.getAsLong() * 1_000);
                                return calendar;
@@ -52,43 +91,47 @@ public class SSOClient {
                @Override
                public void onError(Exception ex) {
                    ex.printStackTrace();
                    SSOClient.invalidateAPICache(ssoAccount);
                    invalidateAPICache(ssoAccount);
                }
            });
            mNextcloudAPIs.put(ssoAccount.name, nextcloudAPI);
            API_CACHE.put(ssoAccount.name, nextcloudAPI);
            return nextcloudAPI;
        }
    }

    /**
     * Invalidates thes API cache for the given ssoAccount
     * Invalidates the API cache for the given {@param ssoAccount}
     *
     * @param ssoAccount the ssoAccount for which the API cache should be cleared.
     */
    public static void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
    public static synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
        Log.v(TAG, "Invalidating API cache for " + ssoAccount.name);
        if (mNextcloudAPIs.containsKey(ssoAccount.name)) {
            final NextcloudAPI nextcloudAPI = mNextcloudAPIs.get(ssoAccount.name);
        if (API_CACHE.containsKey(ssoAccount.name)) {
            final NextcloudAPI nextcloudAPI = API_CACHE.get(ssoAccount.name);
            if (nextcloudAPI != null) {
                nextcloudAPI.stop();
            }
            mNextcloudAPIs.remove(ssoAccount.name);
            API_CACHE.remove(ssoAccount.name);
        }
        API_CACHE_NOTES.remove(ssoAccount.name);
        API_CACHE_OCS.remove(ssoAccount.name);
    }

    /**
     * Invalidates the whole API cache for all accounts
     */
    public static void invalidateAPICache() {
        for (String key : mNextcloudAPIs.keySet()) {
    public static synchronized void invalidateAPICache() {
        for (String key : API_CACHE.keySet()) {
            Log.v(TAG, "Invalidating API cache for " + key);
            if (mNextcloudAPIs.containsKey(key)) {
                final NextcloudAPI nextcloudAPI = mNextcloudAPIs.get(key);
            if (API_CACHE.containsKey(key)) {
                final NextcloudAPI nextcloudAPI = API_CACHE.get(key);
                if (nextcloudAPI != null) {
                    nextcloudAPI.stop();
                }
                mNextcloudAPIs.remove(key);
                API_CACHE.remove(key);
            }
        }
        API_CACHE_NOTES.clear();
        API_CACHE_OCS.clear();
    }
}
+1 −7
Original line number Diff line number Diff line
@@ -7,29 +7,23 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.nextcloud.android.sso.api.NextcloudAPI;
import com.nextcloud.android.sso.api.ParsedResponse;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.model.SingleSignOnAccount;

import java.io.IOException;
import java.util.Map;

import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
import retrofit2.NextcloudRetrofitApiBuilder;

@WorkerThread
public class CapabilitiesClient {

    private static final String TAG = CapabilitiesClient.class.getSimpleName();

    private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
    private static final String HEADER_KEY_ETAG = "ETag";

    public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag) throws Throwable {
        final NextcloudAPI nextcloudAPI = SSOClient.getNextcloudAPI(context.getApplicationContext(), ssoAccount);
        final OcsAPI ocsAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_OCS).create(OcsAPI.class);
        final OcsAPI ocsAPI = ApiProvider.getOcsAPI(context, ssoAccount);
        try {
            final ParsedResponse<Capabilities> response = ocsAPI.getCapabilities(lastETag).blockingSingle();
            final Capabilities capabilities = response.getResponse();
+8 −5
Original line number Diff line number Diff line
@@ -178,10 +178,10 @@ public class NotesRepository {
    @WorkerThread
    public void deleteAccount(@NonNull Account account) {
        try {
            SSOClient.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName()));
            ApiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName()));
        } catch (NextcloudFilesAppAccountNotFoundException e) {
            e.printStackTrace();
            SSOClient.invalidateAPICache();
            ApiProvider.invalidateAPICache();
        }

        db.getAccountDao().deleteAccount(account);
@@ -582,10 +582,13 @@ public class NotesRepository {
                }
                if (apiVersions.length() > 0) {
                    final int updatedRows = db.getAccountDao().updateApiVersion(accountId, apiVersion);
                    if (updatedRows == 1) {
                    if (updatedRows == 0) {
                        Log.d(TAG, "ApiVersion not updated, because it did not change");
                    } else if (updatedRows == 1) {
                        Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId);
                        ApiProvider.invalidateAPICache();
                    } else {
                        Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
                        Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
                    }
                    return true;
                } else {
@@ -899,7 +902,7 @@ public class NotesRepository {
                Log.d(TAG, "No network connection.");
            }
        } catch (NetworkErrorException e) {
            e.printStackTrace();
            Log.i(TAG, e.getMessage());
            networkConnected = false;
            isSyncPossible = false;
        }
Loading