mirror of
https://github.com/nextcloud/notes-android.git
synced 2024-11-22 04:46:04 +03:00
#1167 Use retrofit
This commit is contained in:
parent
ef7e2815b0
commit
4676bb8b6b
6 changed files with 219 additions and 67 deletions
|
@ -98,6 +98,12 @@ dependencies {
|
|||
implementation "androidx.room:room-runtime:2.3.0"
|
||||
annotationProcessor "androidx.room:room-compiler:2.3.0"
|
||||
|
||||
// Retrofit
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
|
||||
|
||||
// Gson
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
// Testing
|
||||
|
|
|
@ -312,6 +312,9 @@ public class NotesRepository {
|
|||
db.getNoteDao().deleteByNoteId(id, forceDBStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
@ -779,10 +782,8 @@ public class NotesRepository {
|
|||
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) {
|
||||
final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, onlyLocalChanges) {
|
||||
@Override
|
||||
void onPreExecute() {
|
||||
syncStatus.postValue(true);
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
package it.niedermann.owncloud.notes.persistence;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nextcloud.android.sso.AccountImporter;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
|
||||
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import it.niedermann.owncloud.notes.persistence.entity.Account;
|
||||
import it.niedermann.owncloud.notes.persistence.entity.Note;
|
||||
import it.niedermann.owncloud.notes.persistence.sync.ApiProvider;
|
||||
import it.niedermann.owncloud.notes.shared.model.DBStatus;
|
||||
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
|
||||
import it.niedermann.owncloud.notes.shared.model.ServerResponse;
|
||||
import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
|
||||
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
|
||||
|
@ -36,8 +44,13 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
|
||||
private static final String TAG = NotesServerSyncTask.class.getSimpleName();
|
||||
|
||||
private static final String HEADER_KEY_X_NOTES_API_VERSIONS = "X-Notes-API-Versions";
|
||||
private static final String HEADER_KEY_ETAG = "ETag";
|
||||
private static final String HEADER_KEY_LAST_MODIFIED = "Last-Modified";
|
||||
@NonNull
|
||||
private final NotesClient notesClient;
|
||||
private final Context context;
|
||||
@NonNull
|
||||
private ApiProvider notesClient;
|
||||
@NonNull
|
||||
private final NotesRepository repo;
|
||||
@NonNull
|
||||
|
@ -50,12 +63,12 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
@NonNull
|
||||
protected final ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||
|
||||
NotesServerSyncTask(@NonNull NotesClient notesClient, @NonNull NotesRepository repo, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
|
||||
NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges) throws NextcloudFilesAppAccountNotFoundException {
|
||||
super(TAG);
|
||||
this.notesClient = notesClient;
|
||||
this.context = context;
|
||||
this.repo = repo;
|
||||
this.localAccount = localAccount;
|
||||
this.ssoAccount = ssoAccount;
|
||||
this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName());
|
||||
this.onlyLocalChanges = onlyLocalChanges;
|
||||
}
|
||||
|
||||
|
@ -67,15 +80,30 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
public void run() {
|
||||
onPreExecute();
|
||||
|
||||
Log.i(TAG, "STARTING SYNCHRONIZATION");
|
||||
final SyncResultStatus status = new SyncResultStatus();
|
||||
status.pushSuccessful = pushLocalChanges();
|
||||
if (!onlyLocalChanges) {
|
||||
status.pullSuccessful = pullRemoteChanges();
|
||||
}
|
||||
Log.i(TAG, "SYNCHRONIZATION FINISHED");
|
||||
notesClient = new ApiProvider(context, ssoAccount, new NextcloudAPI.ApiConnectedListener() {
|
||||
@Override
|
||||
public void onConnected() {
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "STARTING SYNCHRONIZATION");
|
||||
final SyncResultStatus status = new SyncResultStatus();
|
||||
status.pushSuccessful = pushLocalChanges();
|
||||
if (!onlyLocalChanges) {
|
||||
status.pullSuccessful = pullRemoteChanges();
|
||||
}
|
||||
Log.i(TAG, "SYNCHRONIZATION FINISHED");
|
||||
|
||||
onPostExecute(status);
|
||||
onPostExecute(status);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
final SyncResultStatus status = new SyncResultStatus();
|
||||
status.pullSuccessful = false;
|
||||
status.pushSuccessful = false;
|
||||
onPostExecute(status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
abstract void onPreExecute();
|
||||
|
@ -99,20 +127,31 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
Log.v(TAG, " ...create/edit");
|
||||
if (note.getRemoteId() != null) {
|
||||
Log.v(TAG, " ...Note has remoteId → try to edit");
|
||||
try {
|
||||
remoteNote = notesClient.editNote(ssoAccount, note).getNote();
|
||||
} catch (NextcloudHttpRequestFailedException e) {
|
||||
if (e.getStatusCode() == HTTP_NOT_FOUND) {
|
||||
final Response<Note> editResponse = notesClient.getNotesAPI().editNote(note, note.getRemoteId()).execute();
|
||||
if (editResponse.isSuccessful()) {
|
||||
remoteNote = editResponse.body();
|
||||
} else {
|
||||
if (editResponse.code() == HTTP_NOT_FOUND) {
|
||||
Log.v(TAG, " ...Note does no longer exist on server → recreate");
|
||||
remoteNote = notesClient.createNote(ssoAccount, note).getNote();
|
||||
final Response<Note> createResponse = notesClient.getNotesAPI().createNote(note).execute();
|
||||
if (createResponse.isSuccessful()) {
|
||||
remoteNote = createResponse.body();
|
||||
} else {
|
||||
throw new Exception(createResponse.errorBody().string());
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
throw new Exception(editResponse.errorBody().string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.v(TAG, " ...Note does not have a remoteId yet → create");
|
||||
remoteNote = notesClient.createNote(ssoAccount, note).getNote();
|
||||
repo.updateRemoteId(note.getId(), remoteNote.getRemoteId());
|
||||
final Response<Note> createResponse = notesClient.getNotesAPI().createNote(note).execute();
|
||||
if (createResponse.isSuccessful()) {
|
||||
remoteNote = createResponse.body();
|
||||
repo.updateRemoteId(note.getId(), remoteNote.getRemoteId());
|
||||
} else {
|
||||
throw new Exception(createResponse.errorBody().string());
|
||||
}
|
||||
}
|
||||
// Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
|
||||
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());
|
||||
|
@ -122,13 +161,12 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
Log.v(TAG, " ...delete (only local, since it has never been synchronized)");
|
||||
} else {
|
||||
Log.v(TAG, " ...delete (from server and local)");
|
||||
try {
|
||||
notesClient.deleteNote(ssoAccount, note.getRemoteId());
|
||||
} catch (NextcloudHttpRequestFailedException e) {
|
||||
if (e.getStatusCode() == HTTP_NOT_FOUND) {
|
||||
final Response<Note> deleteResponse = notesClient.getNotesAPI().deleteNote(note.getRemoteId()).execute();
|
||||
if (!deleteResponse.isSuccessful()) {
|
||||
if (deleteResponse.code() == HTTP_NOT_FOUND) {
|
||||
Log.v(TAG, " ...delete (note has already been deleted remotely)");
|
||||
} else {
|
||||
throw e;
|
||||
throw new Exception(deleteResponse.errorBody().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -173,51 +211,71 @@ abstract class NotesServerSyncTask extends Thread {
|
|||
localAccount.setModified(accountFromDatabase.getModified());
|
||||
localAccount.setETag(accountFromDatabase.getETag());
|
||||
|
||||
final ServerResponse.NotesResponse response = notesClient.getNotes(ssoAccount, localAccount.getModified(), localAccount.getETag());
|
||||
final List<Note> remoteNotes = response.getNotes();
|
||||
final Set<Long> remoteIDs = new HashSet<>();
|
||||
// pull remote changes: update or create each remote note
|
||||
for (Note remoteNote : remoteNotes) {
|
||||
Log.v(TAG, " Process Remote Note: " + remoteNote);
|
||||
remoteIDs.add(remoteNote.getRemoteId());
|
||||
if (remoteNote.getModified() == null) {
|
||||
Log.v(TAG, " ... unchanged");
|
||||
} else if (idMap.containsKey(remoteNote.getRemoteId())) {
|
||||
Log.v(TAG, " ... found → Update");
|
||||
Long localId = idMap.get(remoteNote.getRemoteId());
|
||||
if (localId != null) {
|
||||
repo.updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
|
||||
localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
|
||||
final Response<List<Note>> fetchResponse = notesClient.getNotesAPI().getNotes(localAccount.getModified(), localAccount.getETag()).execute();
|
||||
if (fetchResponse.isSuccessful()) {
|
||||
final List<Note> remoteNotes = fetchResponse.body();
|
||||
final Set<Long> remoteIDs = new HashSet<>();
|
||||
// pull remote changes: update or create each remote note
|
||||
for (Note remoteNote : remoteNotes) {
|
||||
Log.v(TAG, " Process Remote Note: " + remoteNote);
|
||||
remoteIDs.add(remoteNote.getRemoteId());
|
||||
if (remoteNote.getModified() == null) {
|
||||
Log.v(TAG, " ... unchanged");
|
||||
} else if (idMap.containsKey(remoteNote.getRemoteId())) {
|
||||
Log.v(TAG, " ... found → Update");
|
||||
Long localId = idMap.get(remoteNote.getRemoteId());
|
||||
if (localId != null) {
|
||||
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.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote);
|
||||
Log.v(TAG, " ... create");
|
||||
repo.addNote(localAccount.getId(), remoteNote);
|
||||
}
|
||||
} else {
|
||||
Log.v(TAG, " ... create");
|
||||
repo.addNote(localAccount.getId(), remoteNote);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
|
||||
// remove remotely deleted notes (only those without local changes)
|
||||
for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
|
||||
if (!remoteIDs.contains(entry.getKey())) {
|
||||
Log.v(TAG, " ... remove " + entry.getValue());
|
||||
repo.deleteByNoteId(entry.getValue(), DBStatus.VOID);
|
||||
Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
|
||||
// remove remotely deleted notes (only those without local changes)
|
||||
for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
|
||||
if (!remoteIDs.contains(entry.getKey())) {
|
||||
Log.v(TAG, " ... remove " + entry.getValue());
|
||||
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());
|
||||
repo.updateETag(localAccount.getId(), localAccount.getETag());
|
||||
repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
|
||||
try {
|
||||
if (repo.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
|
||||
localAccount.setApiVersion(response.getSupportedApiVersions());
|
||||
// update ETag and Last-Modified in order to reduce size of next response
|
||||
localAccount.setETag(fetchResponse.headers().get(HEADER_KEY_ETAG));
|
||||
|
||||
final Calendar lastModified = Calendar.getInstance();
|
||||
lastModified.setTimeInMillis(0);
|
||||
final String lastModifiedHeader = fetchResponse.headers().get(HEADER_KEY_LAST_MODIFIED);
|
||||
if (lastModifiedHeader != null)
|
||||
lastModified.setTimeInMillis(Date.parse(lastModifiedHeader));
|
||||
Log.d(TAG, "ETag: " + fetchResponse.headers().get(HEADER_KEY_ETAG) + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
|
||||
|
||||
localAccount.setModified(lastModified);
|
||||
|
||||
repo.updateETag(localAccount.getId(), localAccount.getETag());
|
||||
repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
|
||||
|
||||
|
||||
String supportedApiVersions = null;
|
||||
final String supportedApiVersionsHeader = fetchResponse.headers().get(HEADER_KEY_X_NOTES_API_VERSIONS);
|
||||
if (supportedApiVersionsHeader != null) {
|
||||
supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader) + "]";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
try {
|
||||
if (repo.updateApiVersion(localAccount.getId(), supportedApiVersions)) {
|
||||
localAccount.setApiVersion(supportedApiVersions);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception(fetchResponse.errorBody().string());
|
||||
}
|
||||
return true;
|
||||
} catch (NextcloudHttpRequestFailedException e) {
|
||||
Log.d(TAG, "Server returned HTTP Status Code " + e.getStatusCode() + " - " + e.getMessage());
|
||||
if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
|
||||
|
|
|
@ -9,6 +9,9 @@ import androidx.room.Ignore;
|
|||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Calendar;
|
||||
|
||||
|
@ -34,31 +37,51 @@ import it.niedermann.owncloud.notes.shared.model.Item;
|
|||
}
|
||||
)
|
||||
public class Note implements Serializable, Item {
|
||||
@SerializedName("localId")
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
|
||||
@Nullable
|
||||
@Expose
|
||||
@SerializedName("id")
|
||||
private Long remoteId;
|
||||
|
||||
private long accountId;
|
||||
|
||||
@NonNull
|
||||
private DBStatus status = DBStatus.VOID;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(defaultValue = "")
|
||||
@Expose
|
||||
private String title = "";
|
||||
|
||||
@NonNull
|
||||
@Expose
|
||||
@ColumnInfo(defaultValue = "")
|
||||
private String category = "";
|
||||
|
||||
@Expose
|
||||
@Nullable
|
||||
private Calendar modified;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(defaultValue = "")
|
||||
@Expose
|
||||
private String content = "";
|
||||
|
||||
@Expose
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
private boolean favorite = false;
|
||||
|
||||
@Expose
|
||||
@Nullable
|
||||
private String eTag;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(defaultValue = "")
|
||||
private String excerpt = "";
|
||||
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
private int scrollY = 0;
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package it.niedermann.owncloud.notes.persistence.sync;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
import retrofit2.NextcloudRetrofitApiBuilder;
|
||||
|
||||
/**
|
||||
* Created by david on 26.05.17.
|
||||
*/
|
||||
public class ApiProvider {
|
||||
|
||||
private static final String API_ENDPOINT_NOTES = "/index.php/apps/notes/api/v1/";
|
||||
|
||||
private final NotesAPI notesAPI;
|
||||
|
||||
public ApiProvider(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @NonNull final NextcloudAPI.ApiConnectedListener callback) {
|
||||
final NextcloudAPI nextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create(), callback);
|
||||
notesAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, API_ENDPOINT_NOTES).create(NotesAPI.class);
|
||||
}
|
||||
|
||||
public NotesAPI getNotesAPI() {
|
||||
return notesAPI;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package it.niedermann.owncloud.notes.persistence.sync;
|
||||
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import it.niedermann.owncloud.notes.persistence.entity.Note;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
/**
|
||||
* @link <a href="https://deck.readthedocs.io/en/latest/API/">Deck REST API</a>
|
||||
*/
|
||||
public interface NotesAPI {
|
||||
|
||||
@GET("notes")
|
||||
Call<List<Note>> getNotes(@Query("pruneBefore") Calendar lastModified, @Query("If-None-Match") String lastETag);
|
||||
|
||||
@POST("notes")
|
||||
Call<Note> createNote(@Body Note note);
|
||||
|
||||
@PUT("notes/{remoteId}")
|
||||
Call<Note> editNote(@Body Note note, @Path("remoteId") long remoteId);
|
||||
|
||||
@DELETE("notes/{remoteId}")
|
||||
Call<Note> deleteNote(@Path("remoteId") long noteId);
|
||||
|
||||
}
|
Loading…
Reference in a new issue