diff --git a/app/src/androidTest/java/it/niedermann/owncloud/notes/model/NoteTest.java b/app/src/androidTest/java/it/niedermann/owncloud/notes/model/NoteTest.java index 6d6f2dece..c98281312 100644 --- a/app/src/androidTest/java/it/niedermann/owncloud/notes/model/NoteTest.java +++ b/app/src/androidTest/java/it/niedermann/owncloud/notes/model/NoteTest.java @@ -11,7 +11,7 @@ public class NoteTest extends TestCase { public void testMarkDownStrip() { - Note note = new Note(0, Calendar.getInstance(), "#Title", ""); + OwnCloudNote note = new OwnCloudNote(0, Calendar.getInstance(), "#Title", ""); assertTrue("Title".equals(note.getTitle())); note.setTitle("* Aufzählung"); assertTrue("Aufzählung".equals(note.getTitle())); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java index c1d24d966..dae99314d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java @@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.MenuItem; import android.widget.EditText; @@ -16,28 +17,32 @@ import java.util.concurrent.TimeUnit; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; import it.niedermann.owncloud.notes.util.ICallback; import it.niedermann.owncloud.notes.util.NoteUtil; public class EditNoteActivity extends AppCompatActivity { - private final long DELAY = 1000; // in ms + private static final String LOG_TAG = "EditNote/SAVE"; + private final long DELAY = 2000; // in ms + private final long DELAY_AFTER_SYNC = 5000; // in ms private EditText content = null; - private Note note = null; - private Timer timer = new Timer(); + private DBNote note = null; + private Timer timer, timerNextSync; + private boolean saveActive = false; private ActionBar actionBar; + private NoteSQLiteOpenHelper db; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit); - note = (Note) getIntent().getSerializableExtra( + note = (DBNote) getIntent().getSerializableExtra( NoteActivity.EDIT_NOTE); content = (EditText) findViewById(R.id.editContent); - content.setEnabled(false); content.setText(note.getContent()); content.setEnabled(true); + db = new NoteSQLiteOpenHelper(this); actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(note.getTitle()); @@ -53,86 +58,130 @@ public void beforeTextChanged(CharSequence s, int start, int count, @Override public void onTextChanged(final CharSequence s, int start, int before, int count) { - if (timer != null) + if (timer != null) { timer.cancel(); + timer = null; + } } @Override public void afterTextChanged(final Editable s) { - timer = new Timer(); - timer.schedule(new TimerTask() { - @Override - public void run() { - runOnUiThread(new Runnable() { + if(db.getNoteServerSyncHelper().isSyncPossible()) { + if(timer != null) { + timer.cancel(); + } + if(!saveActive) { + timer = new Timer(); + timer.schedule(new TimerTask() { @Override public void run() { - saveData(); + runOnUiThread(new Runnable() { + @Override + public void run() { + autoSave(); + } + }); } - }); + }, DELAY); } - }, DELAY); + } } }); } @Override public void onBackPressed() { - content.setEnabled(false); - saveData(); - Intent data = new Intent(); - data.setAction(Intent.ACTION_VIEW); - data.putExtra(NoteActivity.EDIT_NOTE, note); - setResult(RESULT_OK, data); - finish(); + saveAndClose(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - content.setEnabled(false); - saveData(); - Intent data = new Intent(); - data.setAction(Intent.ACTION_VIEW); - data.putExtra(NoteActivity.EDIT_NOTE, note); - setResult(RESULT_OK, data); - finish(); + saveAndClose(); return true; } return super.onOptionsItemSelected(item); } - private void saveData() { - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setSubtitle(getResources().getString(R.string.action_edit_saving)); + /** + * Saves all changes and closes the Activity + */ + private void saveAndClose() { + content.setEnabled(false); + if(timer!=null) { + timer.cancel(); + timer = null; + } + if(timerNextSync!=null) { + timerNextSync.cancel(); + timerNextSync = null; + } + saveData(null); + Intent data = new Intent(); + data.setAction(Intent.ACTION_VIEW); + data.putExtra(NoteActivity.EDIT_NOTE, note); + setResult(RESULT_OK, data); + finish(); + } + + /** + * Gets the current content of the EditText field in the UI. + * @return String of the current content. + */ + private String getContent() { + return ((EditText) findViewById(R.id.editContent)).getText().toString(); + } + + /** + * Saves the current changes and show the status in the ActionBar + */ + private void autoSave() { + Log.d(LOG_TAG, "START save+sync"); + saveActive = true; + if (actionBar != null) { + actionBar.setSubtitle(getString(R.string.action_edit_saving)); } - // #74 - note.setModified(Calendar.getInstance()); - note.setContent(((EditText) findViewById(R.id.editContent)).getText().toString()); - // #80 - note.setTitle(NoteUtil.generateNoteTitle(note.getContent())); - NoteSQLiteOpenHelper db = new NoteSQLiteOpenHelper(this); - db.getNoteServerSyncHelper().addCallback(new ICallback() { + final String content = getContent(); + saveData(new ICallback() { @Override public void onFinish() { - runOnUiThread(new Runnable() { + // AFTER SYNCHRONIZATION + Log.d(LOG_TAG, "...sync finished"); + actionBar.setTitle(note.getTitle()); + actionBar.setSubtitle(getResources().getString(R.string.action_edit_saved)); + Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() { @Override public void run() { - getSupportActionBar().setSubtitle(getResources().getString(R.string.action_edit_saved)); - Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() { + runOnUiThread(new Runnable() { @Override public void run() { - runOnUiThread(new Runnable() { - @Override - public void run() { - getSupportActionBar().setSubtitle(null); - } - }); + // AFTER 1 SECOND: set ActionBar to default title + actionBar.setSubtitle(getString(R.string.action_edit_editing)); } - }, 1, TimeUnit.SECONDS); + }); + } + }, 1, TimeUnit.SECONDS); + + timerNextSync = new Timer(); + timerNextSync.schedule(new TimerTask() { + @Override + public void run() { + runOnUiThread(new Runnable() { + @Override + public void run() { + // AFTER "DELAY_AFTER_SYNC" SECONDS: allow next auto-save or start it directly + if(getContent().equals(content)) { + saveActive = false; + Log.d(LOG_TAG, "FINISH, no new changes"); + } else { + Log.d(LOG_TAG, "content has changed meanwhile -> restart save"); + autoSave(); + } + } + }); } - }); + }, DELAY_AFTER_SYNC); /* TODO Notify widgets @@ -143,6 +192,13 @@ public void run() { }*/ } }); - db.updateNoteAndSync(note); + } + + /** + * Save the current state in the database and schedule synchronization if needed. + * @param callback + */ + private void saveData(ICallback callback) { + note = db.updateNoteAndSync(note, getContent(), callback); } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteActivity.java index c1c2f22d2..f93a98278 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteActivity.java @@ -12,13 +12,13 @@ import android.widget.TextView; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; public class NoteActivity extends AppCompatActivity implements View.OnClickListener { public final static String EDIT_NOTE = "it.niedermann.owncloud.notes.edit_note_id"; public final static int EDIT_NOTE_CMD = 1; - private Note note = null; + private DBNote note = null; private int notePosition = 0; private TextView noteContent = null; private ActionBar actionBar = null; @@ -27,10 +27,10 @@ public class NoteActivity extends AppCompatActivity implements View.OnClickListe protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_single_note); - note = (Note) getIntent().getSerializableExtra( + note = (DBNote) getIntent().getSerializableExtra( NotesListViewActivity.SELECTED_NOTE); if (savedInstanceState != null) { - note = (Note) savedInstanceState.getSerializable("note"); + note = (DBNote) savedInstanceState.getSerializable("note"); } notePosition = getIntent().getIntExtra( NotesListViewActivity.SELECTED_NOTE_POSITION, 0); @@ -114,7 +114,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == EDIT_NOTE_CMD) { // Make sure the request was successful if (resultCode == RESULT_OK) { - Note editedNote = (Note) data.getExtras().getSerializable( + DBNote editedNote = (DBNote) data.getExtras().getSerializable( EDIT_NOTE); if (editedNote != null) { note = editedNote; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java index 4813188a3..ed1892642 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java @@ -12,21 +12,24 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.widget.Toast; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.model.Item; import it.niedermann.owncloud.notes.model.ItemAdapter; -import it.niedermann.owncloud.notes.model.Note; import it.niedermann.owncloud.notes.model.SectionItem; import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; import it.niedermann.owncloud.notes.util.ICallback; +import it.niedermann.owncloud.notes.util.NotesClientUtil; public class NotesListViewActivity extends AppCompatActivity implements ItemAdapter.NoteClickListener, View.OnClickListener { @@ -48,6 +51,19 @@ public class NotesListViewActivity extends AppCompatActivity implements private NoteSQLiteOpenHelper db = null; private SearchView searchView = null; + private ICallback syncCallBack = new ICallback() { + @Override + public void onFinish() { + adapter.clearSelection(); + if (mActionMode != null) { + mActionMode.finish(); + } + // adapter.checkForUpdates(db.getNotes()); // FIXME deactivated, since it doesn't remove remotely deleted notes + setListView(db.getNotes()); + swipeRefreshLayout.setRefreshing(false); + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -70,20 +86,15 @@ protected void onCreate(Bundle savedInstanceState) { swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { - db.getNoteServerSyncHelper().addCallback(new ICallback() { - @Override - public void onFinish() { - swipeRefreshLayout.setRefreshing(false); - adapter.clearSelection(); - if (mActionMode != null) { - mActionMode.finish(); - } - setListView(db.getNotes()); - } - }); - db.getNoteServerSyncHelper().downloadNotes(); + if(db.getNoteServerSyncHelper().isSyncPossible()) { + synchronize(); + } else { + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getApplicationContext(), getString(R.string.error_sync, getString(NotesClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show(); + } } }); + db.getNoteServerSyncHelper().addCallbackPull(syncCallBack); // Floating Action Button findViewById(R.id.fab_create).setOnClickListener(this); @@ -94,19 +105,8 @@ public void onFinish() { */ @Override protected void onResume() { - db.getNoteServerSyncHelper().addCallback(new ICallback() { - @Override - public void onFinish() { - swipeRefreshLayout.setRefreshing(false); - adapter.clearSelection(); - if (mActionMode != null) { - mActionMode.finish(); - } - adapter.checkForUpdates(db.getNotes()); - } - }); - if (!PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getBoolean(SettingsActivity.SETTINGS_FIRST_RUN, true)) { - db.getNoteServerSyncHelper().downloadNotes(); + if (db.getNoteServerSyncHelper().isSyncPossible() && !PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getBoolean(SettingsActivity.SETTINGS_FIRST_RUN, true)) { + synchronize(); } super.onResume(); } @@ -130,7 +130,7 @@ public void onClick(View v) { * @param noteList List<Note> */ @SuppressWarnings("WeakerAccess") - public void setListView(List noteList) { + public void setListView(List noteList) { List itemList = new ArrayList<>(); // #12 Create Sections depending on Time // TODO Move to ItemAdapter? @@ -161,7 +161,7 @@ public void setListView(List noteList) { month.set(Calendar.SECOND, 0); month.set(Calendar.MILLISECOND, 0); for (int i = 0; i < noteList.size(); i++) { - Note currentNote = noteList.get(i); + DBNote currentNote = noteList.get(i); if (!todaySet && recent.getTimeInMillis() - currentNote.getModified().getTimeInMillis() >= 600000 && currentNote.getModified().getTimeInMillis() >= today.getTimeInMillis()) { // < 10 minutes but after 00:00 today //if (i > 0) { @@ -290,7 +290,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { //not need because of db.synchronisation in createActivity - Note createdNote = (Note) data.getExtras().getSerializable( + DBNote createdNote = (DBNote) data.getExtras().getSerializable( CREATED_NOTE); adapter.add(createdNote); //setListView(db.getNotes()); @@ -301,7 +301,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { SELECTED_NOTE_POSITION); adapter.remove(adapter.getItem(notePosition)); if (resultCode == RESULT_OK) { - Note editedNote = (Note) data.getExtras().getSerializable( + DBNote editedNote = (DBNote) data.getExtras().getSerializable( NoteActivity.EDIT_NOTE); adapter.add(editedNote); } @@ -309,16 +309,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } else if (requestCode == server_settings) { // Create new Instance with new URL and credentials db = new NoteSQLiteOpenHelper(this); - adapter.removeAll(); - swipeRefreshLayout.setRefreshing(true); - db.getNoteServerSyncHelper().addCallback(new ICallback() { - @Override - public void onFinish() { - setListView(db.getNotes()); - swipeRefreshLayout.setRefreshing(false); - } - }); - db.synchronizeWithServer(); + if(db.getNoteServerSyncHelper().isSyncPossible()) { + adapter.removeAll(); + swipeRefreshLayout.setRefreshing(true); + synchronize(); + } else { + Toast.makeText(getApplicationContext(), getString(R.string.error_sync, getString(NotesClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show(); + } } } @@ -354,7 +351,7 @@ public void onNoteClick(int position, View v) { NoteActivity.class); Item item = adapter.getItem(position); - intent.putExtra(SELECTED_NOTE, (Note) item); + intent.putExtra(SELECTED_NOTE, (DBNote) item); intent.putExtra(SELECTED_NOTE_POSITION, position); startActivityForResult(intent, show_single_note_cmd); @@ -382,6 +379,11 @@ public void onBackPressed() { } } + private void synchronize() { + db.getNoteServerSyncHelper().addCallbackPull(syncCallBack); + db.getNoteServerSyncHelper().scheduleSync(false); + } + /** * Handler for the MultiSelect Actions */ @@ -412,7 +414,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { case R.id.menu_delete: List selection = adapter.getSelected(); for (Integer i : selection) { - Note note = (Note) adapter.getItem(i); + DBNote note = (DBNote) adapter.getItem(i); db.deleteNoteAndSync(note.getId()); // Not needed because of dbsync //adapter.remove(note); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java index 7bcf60cac..0b171f1d1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java @@ -15,9 +15,9 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.android.widget.SingleNoteWidget; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.model.Item; import it.niedermann.owncloud.notes.model.ItemAdapter; -import it.niedermann.owncloud.notes.model.Note; import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; /** @@ -38,7 +38,7 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_select_single_note); // Display Data db = new NoteSQLiteOpenHelper(this); - db.synchronizeWithServer(); + db.getNoteServerSyncHelper().scheduleSync(false); setListView(db.getNotes()); @@ -61,7 +61,7 @@ public void onCreate(Bundle savedInstanceState) { * * @param noteList List<Note> */ - private void setListView(List noteList) { + private void setListView(List noteList) { List itemList = new ArrayList<>(); itemList.addAll(noteList); adapter = new ItemAdapter(itemList); @@ -79,7 +79,7 @@ public void onNoteClick(int position, View v) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_single_note); appWidgetManager.updateAppWidget(appWidgetId, views); - SingleNoteWidget.updateAppWidget((Note) adapter.getItem(position), context, appWidgetManager, appWidgetId); + SingleNoteWidget.updateAppWidget((DBNote) adapter.getItem(position), context, appWidgetManager, appWidgetId); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/widget/AllNotesWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/android/widget/AllNotesWidget.java index e85c7e645..726c335f9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/widget/AllNotesWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/widget/AllNotesWidget.java @@ -13,7 +13,8 @@ import java.util.List; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.DBNote; +import it.niedermann.owncloud.notes.model.DBStatus; import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; /** @@ -51,7 +52,7 @@ public RemoteViewsFactory onGetViewFactory(Intent intent) { class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { private static final int mCount = 10; - private List mWidgetItems = new ArrayList<>(); + private List mWidgetItems = new ArrayList<>(); private Context mContext; private int mAppWidgetId; @@ -60,13 +61,13 @@ public StackRemoteViewsFactory(Context context, Intent intent) { mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); NoteSQLiteOpenHelper db = new NoteSQLiteOpenHelper(mContext); - db.synchronizeWithServer(); + db.getNoteServerSyncHelper().scheduleSync(false); mWidgetItems = db.getNotes(); - mWidgetItems.add(new Note(0, Calendar.getInstance(), "Test-Titel", "Test-Beschreibung")); + mWidgetItems.add(new DBNote(0, 0, Calendar.getInstance(), "Test-Titel", "Test-Beschreibung", DBStatus.VOID)); } public void onCreate() { - mWidgetItems.add(new Note(0, Calendar.getInstance(), "Test-Titel", "Test-Beschreibung")); + mWidgetItems.add(new DBNote(0, 0, Calendar.getInstance(), "Test-Titel", "Test-Beschreibung", DBStatus.VOID)); } @Override diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/widget/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/android/widget/SingleNoteWidget.java index d2c695629..26593ff4b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/widget/SingleNoteWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/android/widget/SingleNoteWidget.java @@ -12,7 +12,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.android.activity.NoteActivity; import it.niedermann.owncloud.notes.android.activity.NotesListViewActivity; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.DBNote; /** * Widget which displays a single selected note. @@ -20,7 +20,7 @@ */ public class SingleNoteWidget extends AppWidgetProvider { - public static void updateAppWidget(Note note, Context context, AppWidgetManager appWidgetManager, int appWidgetId) { + public static void updateAppWidget(DBNote note, Context context, AppWidgetManager appWidgetManager, int appWidgetId) { RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_single_note); if (note != null) { updateViews.setTextViewText(R.id.single_note_content, note.getSpannableContent()); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java b/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java new file mode 100644 index 000000000..f25105b86 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java @@ -0,0 +1,65 @@ +package it.niedermann.owncloud.notes.model; + +import java.io.Serializable; +import java.util.Calendar; + +import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; +import it.niedermann.owncloud.notes.util.NoteUtil; + +/** + * DBNote represents a single note from the local SQLite database with all attributes. + * It extends OwnCloudNote with attributes required for local data management. + */ +public class DBNote extends OwnCloudNote implements Item, Serializable { + + private long id; + private DBStatus status; + private String excerpt = ""; + + public DBNote(long id, long remoteId, Calendar modified, String title, String content, DBStatus status) { + super(remoteId, modified, title, content); + this.id = id; + setExcerpt(content); + this.status = status; + } + + public long getId() { + return id; + } + + public DBStatus getStatus() { + return status; + } + + public void setStatus(DBStatus status) { + this.status = status; + } + + public String getExcerpt() { + return excerpt; + } + + private void setExcerpt(String content) { + excerpt = NoteUtil.generateNoteExcerpt(content); + } + + public CharSequence getSpannableContent() { + // TODO Cache the generated CharSequence not possible because CharSequence does not implement Serializable + return NoteUtil.parseMarkDown(getContent()); + } + + public void setContent(String content) { + super.setContent(content); + setExcerpt(content); + } + + @Override + public boolean isSection() { + return false; + } + + @Override + public String toString() { + return "#" + getId() + "/R"+getRemoteId()+" " + getTitle() + " (" + getModified(NoteSQLiteOpenHelper.DATE_FORMAT) + ") " + getStatus(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java index fc27f8265..6ca063188 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java @@ -6,7 +6,29 @@ */ public enum DBStatus { - VOID(""), LOCAL_CREATED("LOCAL_CREATED"), LOCAL_EDITED("LOCAL_EDITED"), LOCAL_DELETED("LOCAL_DELETED"); + /** + * VOID means, that the Note was not modified locally + */ + VOID(""), + + /** + * LOCAL_CREATED is not used anymore, since a newly created note has REMOTE_ID=0 + */ + @Deprecated + LOCAL_CREATED("LOCAL_CREATED"), + + /** + * LOCAL_EDITED means that a Note was created and/or changed since the last successful synchronization. + * If it was newly created, then REMOTE_ID is 0 + */ + LOCAL_EDITED("LOCAL_EDITED"), + + /** + * LOCAL_DELETED means that the Note was deleted locally, but this information was not yet synchronized. + * Therefore, the Note have to be kept locally until the synchronization has succeeded. + * However, Notes with this status should not be displayed in the UI. + */ + LOCAL_DELETED("LOCAL_DELETED"); private final String title; @@ -17,4 +39,17 @@ public String getTitle() { DBStatus(String title) { this.title = title; } + + /** + * Parse a String an get the appropriate DBStatus enum element. + * @param str The String containing the DBStatus identifier. Must not null. + * @return The DBStatus fitting to the String. + */ + public static DBStatus parse(String str) { + if(str.isEmpty()) { + return DBStatus.VOID; + } else { + return DBStatus.valueOf(str); + } + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java index b0bc49e9f..f7a291049 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java @@ -4,6 +4,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; @@ -37,7 +38,7 @@ public static void setNoteClickListener(NoteClickListener noteClickListener) { * Adds the given note to the top of the list. * @param note Note that should be added. */ - public void add(Note note) { + public void add(DBNote note) { itemList.add(0, note); notifyItemInserted(0); notifyItemChanged(0); @@ -55,12 +56,12 @@ public void removeAll() { * Compares the given List of notes to the current internal holded notes and updates the list if necessairy * @param newNotes List of more up to date notes */ - public void checkForUpdates(List newNotes) { - for(Note newNote : newNotes) { + public void checkForUpdates(List newNotes) { + for(DBNote newNote : newNotes) { boolean foundNewNoteInOldList = false; for(Item oldItem : itemList) { if(!oldItem.isSection()) { - Note oldNote = (Note) oldItem; + DBNote oldNote = (DBNote) oldItem; if(newNote.getId() == oldNote.getId()) { // Notes have the same id, check which is newer if(newNote.getModified().after(oldNote.getModified())) { @@ -109,9 +110,10 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ((SectionViewHolder) holder).sectionTitle.setText(section.geTitle()); ((SectionViewHolder) holder).setPosition(position); } else { - Note note = (Note) item; + DBNote note = (DBNote) item; ((NoteViewHolder) holder).noteTitle.setText(note.getTitle()); ((NoteViewHolder) holder).noteExcerpt.setText(note.getExcerpt()); + ((NoteViewHolder) holder).noteStatus.setVisibility(DBStatus.VOID.equals(note.getStatus()) ? View.GONE : View.VISIBLE); ((NoteViewHolder) holder).setPosition(position); } } @@ -168,12 +170,14 @@ public static class NoteViewHolder extends RecyclerView.ViewHolder implements Vi // each data item is just a string in this case public TextView noteTitle; public TextView noteExcerpt; + public ImageView noteStatus; public int position = -1; private NoteViewHolder(View v) { super(v); this.noteTitle = (TextView) v.findViewById(R.id.noteTitle); this.noteExcerpt = (TextView) v.findViewById(R.id.noteExcerpt); + this.noteStatus = (ImageView) v.findViewById(R.id.noteStatus); v.setOnClickListener(this); v.setOnLongClickListener(this); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/Note.java b/app/src/main/java/it/niedermann/owncloud/notes/model/OwnCloudNote.java similarity index 59% rename from app/src/main/java/it/niedermann/owncloud/notes/model/Note.java rename to app/src/main/java/it/niedermann/owncloud/notes/model/OwnCloudNote.java index 4faf03a17..b9a31865c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/Note.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/model/OwnCloudNote.java @@ -8,16 +8,18 @@ import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper; import it.niedermann.owncloud.notes.util.NoteUtil; -@SuppressWarnings("serial") -public class Note implements Item, Serializable { - private long id = 0; +/** + * OwnCloudNote represents a remote note from an OwnCloud server. + * It can be directly generated from the JSON answer from the server. + */ +public class OwnCloudNote implements Serializable { + private long remoteId = 0; private String title = ""; private Calendar modified = null; private String content = ""; - private String excerpt = ""; - public Note(long id, Calendar modified, String title, String content) { - this.id = id; + public OwnCloudNote(long remoteId, Calendar modified, String title, String content) { + this.remoteId = remoteId; if (title != null) setTitle(title); setTitle(title); @@ -25,8 +27,12 @@ public Note(long id, Calendar modified, String title, String content) { this.modified = modified; } - public long getId() { - return id; + public long getRemoteId() { + return remoteId; + } + + public void setRemoteId(long remoteId) { + this.remoteId = remoteId; } public String getTitle() { @@ -56,30 +62,11 @@ public String getContent() { } public void setContent(String content) { - setExcerpt(content); this.content = content; } - public String getExcerpt() { - return excerpt; - } - - private void setExcerpt(String content) { - excerpt = NoteUtil.generateNoteExcerpt(content); - } - - public CharSequence getSpannableContent() { - // TODO Cache the generated CharSequence not possible because CharSequence does not implement Serializable - return NoteUtil.parseMarkDown(getContent()); - } - @Override public String toString() { - return "#" + getId() + " " + getTitle() + " (" + getModified(NoteSQLiteOpenHelper.DATE_FORMAT) + ")"; - } - - @Override - public boolean isSection() { - return false; + return "#" + getRemoteId() + " " + getTitle() + " (" + getModified(NoteSQLiteOpenHelper.DATE_FORMAT) + ")"; } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSQLiteOpenHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSQLiteOpenHelper.java index d554bd49d..478910d93 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSQLiteOpenHelper.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSQLiteOpenHelper.java @@ -5,6 +5,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -13,8 +14,10 @@ import java.util.List; import java.util.Locale; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.model.DBStatus; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.OwnCloudNote; +import it.niedermann.owncloud.notes.util.ICallback; import it.niedermann.owncloud.notes.util.NoteUtil; /** @@ -25,23 +28,24 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper { public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; - private static final int database_version = 4; + private static final int database_version = 5; private static final String database_name = "OWNCLOUD_NOTES"; private static final String table_notes = "NOTES"; private static final String key_id = "ID"; + private static final String key_remote_id = "REMOTEID"; private static final String key_status = "STATUS"; private static final String key_title = "TITLE"; private static final String key_modified = "MODIFIED"; private static final String key_content = "CONTENT"; - private static final String[] columns = {key_id, key_status, key_title, key_modified, key_content}; + private static final String[] columns = {key_id, key_remote_id, key_status, key_title, key_modified, key_content}; private NoteServerSyncHelper serverSyncHelper = null; private Context context = null; public NoteSQLiteOpenHelper(Context context) { super(context, database_name, null, database_version); - this.context = context; - serverSyncHelper = new NoteServerSyncHelper(this); + this.context = context.getApplicationContext(); + serverSyncHelper = NoteServerSyncHelper.getInstance(this); } public NoteServerSyncHelper getNoteServerSyncHelper() { @@ -57,10 +61,32 @@ public NoteServerSyncHelper getNoteServerSyncHelper() { public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE '" + table_notes + "' ( '" + key_id + "' INTEGER PRIMARY KEY AUTOINCREMENT, '" + + key_remote_id + "' INTEGER, '" + key_status + "' VARCHAR(50), '" + key_title + "' TEXT, '" + key_modified + "' TEXT, '" + key_content + "' TEXT)"); + // FIXME create index for status and remote_id + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if(oldVersion<4) { + clearDatabase(db); + } + if(oldVersion<5) { + db.execSQL("ALTER TABLE "+table_notes+" ADD COLUMN "+key_remote_id+" INTEGER"); + db.execSQL("UPDATE "+table_notes+" SET "+key_remote_id+"="+key_id+" WHERE ("+key_remote_id+" IS NULL OR "+key_remote_id+"=0) AND "+key_status+"!=?", new String[]{DBStatus.LOCAL_CREATED.getTitle()}); + db.execSQL("UPDATE "+table_notes+" SET "+key_remote_id+"=0, "+key_status+"=? WHERE "+key_status+"=?", new String[]{DBStatus.LOCAL_EDITED.getTitle(), DBStatus.LOCAL_CREATED.getTitle()}); + } + } + + private void clearDatabase(SQLiteDatabase db) { + db.delete(table_notes, null, null); + } + + public Context getContext() { + return context; } /** @@ -70,18 +96,9 @@ public void onCreate(SQLiteDatabase db) { */ @SuppressWarnings("UnusedReturnValue") public long addNoteAndSync(String content) { - SQLiteDatabase db = this.getWritableDatabase(); - - ContentValues values = new ContentValues(); - values.put(key_status, DBStatus.LOCAL_CREATED.getTitle()); - values.put(key_title, NoteUtil.generateNoteTitle(content)); - values.put(key_content, content); - - long id = db.insert(table_notes, - null, - values); - db.close(); - serverSyncHelper.uploadNewNotes(); + DBNote note = new DBNote(0, 0, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, DBStatus.LOCAL_EDITED); + long id = addNote(note); + getNoteServerSyncHelper().scheduleSync(true); return id; } @@ -89,20 +106,29 @@ public long addNoteAndSync(String content) { * Inserts a note directly into the Database. * No Synchronisation will be triggered! Use addNoteAndSync()! * - * @param note Note to be added + * @param note Note to be added. Remotely created Notes must be of type OwnCloudNote and locally created Notes must be of Type DBNote (with DBStatus.LOCAL_EDITED)! */ - public void addNote(Note note) { + long addNote(OwnCloudNote note) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(NoteSQLiteOpenHelper.key_id, note.getId()); - values.put(NoteSQLiteOpenHelper.key_status, DBStatus.VOID.getTitle()); - values.put(NoteSQLiteOpenHelper.key_title, note.getTitle()); - values.put(NoteSQLiteOpenHelper.key_modified, note.getModified(NoteSQLiteOpenHelper.DATE_FORMAT)); - values.put(NoteSQLiteOpenHelper.key_content, note.getContent()); - db.insert(NoteSQLiteOpenHelper.table_notes, - null, - values); + if(note instanceof DBNote) { + DBNote dbNote = (DBNote)note; + if (dbNote.getId() > 0) { + values.put(key_id, dbNote.getId()); + } + values.put(key_status, dbNote.getStatus().getTitle()); + } else { + values.put(key_status, DBStatus.VOID.getTitle()); + } + if(note.getRemoteId()>0) { + values.put(key_remote_id, note.getRemoteId()); + } + values.put(key_title, note.getTitle()); + values.put(key_modified, note.getModified(NoteSQLiteOpenHelper.DATE_FORMAT)); + values.put(key_content, note.getContent()); + long id = db.insert(table_notes, null, values); db.close(); + return id; } /** @@ -112,7 +138,7 @@ public void addNote(Note note) { * @return requested Note */ @SuppressWarnings("unused") - public Note getNote(long id) { + public DBNote getNote(long id) { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor = db.query(table_notes, @@ -123,20 +149,58 @@ public Note getNote(long id) { null, null, null); - if (cursor != null) { - cursor.moveToFirst(); + if (cursor == null) { + return null; + } + cursor.moveToFirst(); + DBNote note = getNoteFromCursor(cursor); + cursor.close(); + return note; + } + + /** + * Query the database with a custom raw query. + * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). + * @param selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in order that they appear in the selection. The values will be bound as Strings. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered. + * @return List of Notes + */ + private List getNotesCustom(String selection, String[] selectionArgs, String orderBy) { + SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.query(table_notes, columns, selection, selectionArgs, null, null, orderBy); + List notes = new ArrayList<>(); + if (cursor.moveToFirst()) { + do { + notes.add(getNoteFromCursor(cursor)); + } while (cursor.moveToNext()); } + cursor.close(); + return notes; + } + + /** + * Creates a DBNote object from the current row of a Cursor. + * @param cursor database cursor + * @return DBNote + */ + private DBNote getNoteFromCursor(Cursor cursor) { Calendar modified = Calendar.getInstance(); try { - String modifiedStr = cursor != null ? cursor.getString(3) : null; + String modifiedStr = cursor.getString(4); if (modifiedStr != null) modified.setTime(new SimpleDateFormat(DATE_FORMAT, Locale.GERMANY).parse(modifiedStr)); } catch (ParseException e) { e.printStackTrace(); } - Note note = new Note(Long.valueOf(cursor != null ? cursor.getString(0) : null), modified, cursor != null ? cursor.getString(2) : null, cursor.getString(4)); - cursor.close(); - return note; + return new DBNote(cursor.getLong(0), cursor.getLong(1), modified, cursor.getString(3), cursor.getString(5), DBStatus.parse(cursor.getString(2))); + } + + public void debugPrintFullDB() { + List notes = getNotesCustom("", new String[]{}, key_modified + " DESC"); + Log.d(getClass().getSimpleName(), "Full Database:"); + for (DBNote note : notes) { + Log.d(getClass().getSimpleName(), " "+note); + } } /** @@ -144,25 +208,8 @@ public Note getNote(long id) { * * @return List<Note> */ - public List getNotes() { - List notes = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.rawQuery("SELECT * FROM " + table_notes + " WHERE " + key_status + " != ? ORDER BY " + key_modified + " DESC", new String[]{DBStatus.LOCAL_DELETED.getTitle()}); - if (cursor.moveToFirst()) { - do { - Calendar modified = Calendar.getInstance(); - try { - String modifiedStr = cursor.getString(3); - if (modifiedStr != null) - modified.setTime(new SimpleDateFormat(DATE_FORMAT, Locale.GERMANY).parse(modifiedStr)); - } catch (ParseException e) { - e.printStackTrace(); - } - notes.add(new Note(Long.valueOf(cursor.getString(0)), modified, cursor.getString(2), cursor.getString(4))); - } while (cursor.moveToNext()); - } - cursor.close(); - return notes; + public List getNotes() { + return getNotesCustom(key_status + " != ?", new String[]{DBStatus.LOCAL_DELETED.getTitle()}, key_modified + " DESC"); } /** @@ -170,25 +217,8 @@ public List getNotes() { * * @return List<Note> */ - public List searchNotes(String query) { - List notes = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.rawQuery("SELECT * FROM " + table_notes + " WHERE " + key_status + " != ? AND " + key_content + " LIKE ? ORDER BY " + key_modified + " DESC", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "%" + query + "%"}); - if (cursor.moveToFirst()) { - do { - Calendar modified = Calendar.getInstance(); - try { - String modifiedStr = cursor.getString(3); - if (modifiedStr != null) - modified.setTime(new SimpleDateFormat(DATE_FORMAT, Locale.GERMANY).parse(modifiedStr)); - } catch (ParseException e) { - e.printStackTrace(); - } - notes.add(new Note(Long.valueOf(cursor.getString(0)), modified, cursor.getString(2), cursor.getString(4))); - } while (cursor.moveToNext()); - } - cursor.close(); - return notes; + public List searchNotes(String query) { + return getNotesCustom(key_status + " != ? AND " + key_content + " LIKE ?", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "%" + query + "%"}, key_modified + " DESC"); } /** @@ -196,89 +226,96 @@ public List searchNotes(String query) { * * @return List<Note> */ - public List getNotesByStatus(DBStatus status) { - List notes = new ArrayList<>(); - SQLiteDatabase db = this.getWritableDatabase(); - Cursor cursor = db.rawQuery("SELECT * FROM " + table_notes + " WHERE " + key_status + " = ?", new String[]{status.getTitle()}); - if (cursor.moveToFirst()) { - do { - Calendar modified = Calendar.getInstance(); - try { - String modifiedStr = cursor.getString(3); - if (modifiedStr != null) - modified.setTime(new SimpleDateFormat(DATE_FORMAT, Locale.GERMANY).parse(modifiedStr)); - } catch (ParseException e) { - e.printStackTrace(); - } - notes.add(new Note(Long.valueOf(cursor.getString(0)), modified, cursor.getString(2), cursor.getString(4))); - } while (cursor.moveToNext()); - } - cursor.close(); - return notes; + public List getNotesByStatus(DBStatus status) { + return getNotesCustom(key_status + " = ?", new String[]{status.getTitle()}, null); + } + /** + * Returns a list of all Notes in the Database with were modified locally + * + * @return List<Note> + */ + public List getLocalModifiedNotes() { + return getNotesCustom(key_status + " != ?", new String[]{DBStatus.VOID.getTitle()}, null); } /** - * Updates a single Note and sets a synchronization Flag. + * Updates a single Note with a new content. + * The title is derived from the new content automatically, and modified date as well as DBStatus are updated, too -- if the content differs to the state in the database. * - * @param note Note - Note with the updated Information - * @return The number of the Rows affected. + * @param oldNote Note to be changed + * @param newContent New content + * @param callback When the synchronization is finished, this callback will be invoked (optional). + * @return changed note if differs from database, otherwise the old note. */ - @SuppressWarnings("UnusedReturnValue") - public int updateNoteAndSync(Note note) { + public DBNote updateNoteAndSync(DBNote oldNote, String newContent, ICallback callback) { + debugPrintFullDB(); + DBNote newNote = new DBNote(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), NoteUtil.generateNoteTitle(newContent), newContent, DBStatus.LOCAL_EDITED); SQLiteDatabase db = this.getWritableDatabase(); - DBStatus newStatus = DBStatus.LOCAL_EDITED; - Cursor cursor = - db.query(table_notes, - columns, - key_id + " = ? AND " + key_status + " != ?", - new String[]{String.valueOf(note.getId()), DBStatus.LOCAL_DELETED.getTitle()}, - null, - null, - null, - null); - if (cursor != null) { - cursor.moveToFirst(); - String status = cursor.getString(1); - if (!"".equals(status) && DBStatus.valueOf(status) == DBStatus.LOCAL_CREATED) { - newStatus = DBStatus.LOCAL_CREATED; - } - cursor.close(); - } ContentValues values = new ContentValues(); - values.put(key_id, note.getId()); - values.put(key_status, newStatus.getTitle()); - values.put(key_title, note.getTitle()); - values.put(key_modified, note.getModified(DATE_FORMAT)); - values.put(key_content, note.getContent()); - int i = db.update(table_notes, - values, - key_id + " = ?", - new String[]{String.valueOf(note.getId())}); + values.put(key_status, newNote.getStatus().getTitle()); + values.put(key_title, newNote.getTitle()); + values.put(key_modified, newNote.getModified(DATE_FORMAT)); + values.put(key_content, newNote.getContent()); + int rows = db.update(table_notes, values, key_id + " = ? AND " + key_content + " != ?", new String[]{String.valueOf(newNote.getId()), newNote.getContent()}); db.close(); - serverSyncHelper.uploadEditedNotes(); - return i; + // if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly. + if(rows > 0) { + if(callback!=null) { + serverSyncHelper.addCallbackPush(callback); + } + serverSyncHelper.scheduleSync(true); + return newNote; + } else { + if(callback!=null) { + callback.onFinish(); + } + return oldNote; + } } /** - * Updates a single Note. No Synchronization will be triggered. Use updateNoteAndSync()! + * Updates a single Note with data from the server, (if it was not modified locally). + * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization. + * This is used by the synchronization task, hence no Synchronization will be triggered. Use updateNoteAndSync() instead! + * + * @param id local ID of Note + * @param remoteNote Note from the server. + * @param forceUnchangedDBNoteState is not null, then the local note is updated only if it was not modified meanwhile * - * @param note Note - Note with the updated Information * @return The number of the Rows affected. */ - @SuppressWarnings("UnusedReturnValue") - public int updateNote(Note note) { + int updateNote(long id, OwnCloudNote remoteNote, DBNote forceUnchangedDBNoteState) { SQLiteDatabase db = this.getWritableDatabase(); + + // First, update the remote ID, since this field cannot be changed in parallel, but have to be updated always. ContentValues values = new ContentValues(); - values.put(key_id, note.getId()); + values.put(key_remote_id, remoteNote.getRemoteId()); + db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(id)}); + + // The other columns have to be updated in dependency of forceUnchangedDBNoteState, + // since the Synchronization-Task must not overwrite locales changes! + values.clear(); values.put(key_status, DBStatus.VOID.getTitle()); - values.put(key_title, note.getTitle()); - values.put(key_modified, note.getModified(DATE_FORMAT)); - values.put(key_content, note.getContent()); - int i = db.update(table_notes, - values, - key_id + " = ?", - new String[]{String.valueOf(note.getId())}); + values.put(key_title, remoteNote.getTitle()); + values.put(key_modified, remoteNote.getModified(DATE_FORMAT)); + values.put(key_content, remoteNote.getContent()); + String whereClause; + String[] whereArgs; + if(forceUnchangedDBNoteState!=null) { + // used by: NoteServerSyncHelper.SyncTask.pushLocalChanges() + // update only, if not modified locally during the synchronization, + // uses reference value gathered at start of synchronization + whereClause = key_id + " = ? AND " + key_content + " = ?"; + whereArgs = new String[]{String.valueOf(id), forceUnchangedDBNoteState.getContent()}; + } else { + // used by: NoteServerSyncHelper.SyncTask.pullRemoteChanges() + // update only, if not modified locally + whereClause = key_id + " = ? AND " + key_status + " = ? AND ("+key_modified+"!=? OR "+key_content+"!=?)"; + whereArgs = new String[]{String.valueOf(id), DBStatus.VOID.getTitle(), remoteNote.getModified(DATE_FORMAT), remoteNote.getContent()}; + } + int i = db.update(table_notes, values, whereClause, whereArgs); db.close(); + Log.d(getClass().getSimpleName(), "updateNote: "+remoteNote+" || forceUnchangedDBNoteState: "+forceUnchangedDBNoteState+" => "+i+" rows updated"); return i; } @@ -299,39 +336,22 @@ public int deleteNoteAndSync(long id) { key_id + " = ?", new String[]{String.valueOf(id)}); db.close(); - serverSyncHelper.uploadDeletedNotes(); + getNoteServerSyncHelper().scheduleSync(true); return i; } /** - * Delete a single Note from the Database + * Delete a single Note from the Database, if it has a specific DBStatus. + * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization. * * @param id long - ID of the Note that should be deleted. + * @param forceDBStatus DBStatus, e.g., if Note was marked as LOCAL_DELETED (for NoteSQLiteOpenHelper.SyncTask.pushLocalChanges()) or is unchanged VOID (for NoteSQLiteOpenHelper.SyncTask.pullRemoteChanges()) */ - public void deleteNote(long id) { + void deleteNote(long id, DBStatus forceDBStatus) { SQLiteDatabase db = this.getWritableDatabase(); db.delete(table_notes, - key_id + " = ?", - new String[]{String.valueOf(id)}); + key_id + " = ? AND " + key_status + " = ?", + new String[]{String.valueOf(id), forceDBStatus.getTitle()}); db.close(); } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - clearDatabase(); - } - - public void clearDatabase() { - SQLiteDatabase db = this.getWritableDatabase(); - db.delete(table_notes, null, null); - db.close(); - } - - public Context getContext() { - return context; - } - - public void synchronizeWithServer() { - serverSyncHelper.synchronize(); - } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java index f82db47ab..2690a639a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java @@ -1,26 +1,33 @@ package it.niedermann.owncloud.notes.persistence; -import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.AsyncTask; -import android.os.Handler; -import android.os.Message; import android.preference.PreferenceManager; -import android.view.View; +import android.util.Log; import android.widget.Toast; import org.json.JSONException; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.android.activity.SettingsActivity; +import it.niedermann.owncloud.notes.model.DBNote; import it.niedermann.owncloud.notes.model.DBStatus; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.OwnCloudNote; import it.niedermann.owncloud.notes.util.ICallback; import it.niedermann.owncloud.notes.util.NotesClient; import it.niedermann.owncloud.notes.util.NotesClientUtil.LoginStatus; @@ -32,214 +39,287 @@ */ public class NoteServerSyncHelper { - private NotesClient client = null; - private NoteSQLiteOpenHelper db = null; - private final View.OnClickListener goToSettingsListener = new View.OnClickListener() { + private static NoteServerSyncHelper instance; + + /** + * Get (or create) instance from NoteServerSyncHelper. + * This has to be a singleton in order to realize correct registering and unregistering of + * the BroadcastReceiver, which listens on changes of network connectivity. + * @param dbHelper NoteSQLiteOpenHelper + * @return NoteServerSyncHelper + */ + public static synchronized NoteServerSyncHelper getInstance(NoteSQLiteOpenHelper dbHelper) { + if(instance==null) { + instance = new NoteServerSyncHelper(dbHelper); + } + return instance; + } + + private final NoteSQLiteOpenHelper dbHelper; + private final Context appContext; + + // Track network connection changes using a BroadcastReceiver + private boolean networkConnected = false; + private final BroadcastReceiver networkReceiver = new BroadcastReceiver() { @Override - public void onClick(View v) { - Activity parent = (Activity) db.getContext(); - Intent intent = new Intent(parent, SettingsActivity.class); - parent.startActivity(intent); + public void onReceive(Context context, Intent intent) { + ConnectivityManager connMgr = (ConnectivityManager)appContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); + if(activeInfo != null && activeInfo.isConnected()) { + Log.d(NoteServerSyncHelper.class.getSimpleName(), "Network connection established."); + networkConnected = true; + scheduleSync(false); + } else { + networkConnected = false; + Log.d(NoteServerSyncHelper.class.getSimpleName(), "No network connection."); + } } }; - private int operationsCount = 0; - private int operationsFinished = 0; - private Handler handler = null; - private List callbacks = new ArrayList<>(); - - public NoteServerSyncHelper(NoteSQLiteOpenHelper db) { - this.db = db; - handler = new Handler() { - @Override - public void handleMessage(Message msg) { - for (ICallback callback : callbacks) { - callback.onFinish(); - } - callbacks.clear(); - } - }; - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(db.getContext().getApplicationContext()); - String url = preferences.getString(SettingsActivity.SETTINGS_URL, - SettingsActivity.DEFAULT_SETTINGS); - String username = preferences.getString(SettingsActivity.SETTINGS_USERNAME, - SettingsActivity.DEFAULT_SETTINGS); - String password = preferences.getString(SettingsActivity.SETTINGS_PASSWORD, - SettingsActivity.DEFAULT_SETTINGS); - client = new NotesClient(url, username, password); + + // current state of the synchronization + private boolean syncActive = false; + private boolean syncScheduled = false; + + // list of callbacks for both parts of synchronziation + private List callbacksPush = new ArrayList<>(); + private List callbacksPull = new ArrayList<>(); + + + + private NoteServerSyncHelper(NoteSQLiteOpenHelper db) { + this.dbHelper = db; + this.appContext = db.getContext().getApplicationContext(); + + // Registers BroadcastReceiver to track network connection changes. + appContext.registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + @Override + protected void finalize() throws Throwable { + appContext.unregisterReceiver(networkReceiver); + super.finalize(); } + /** - * Adds a callback method to the NoteServerSyncHelper. - * All callbacks will be executed once all synchronize operations are done. + * Synchronization is only possible, if there is an active network connection. + * NoteServerSyncHelper observes changes in the network connection. + * The current state can be retrieved with this method. + * @return true if sync is possible, otherwise false. + */ + public boolean isSyncPossible() { + return networkConnected; + } + + + /** + * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server. + * All callbacks will be executed once the synchronization operations are done. * After execution the callback will be deleted, so it has to be added again if it shall be * executed the next time all synchronize operations are finished. * * @param callback Implementation of ICallback, contains one method that shall be executed. */ - public void addCallback(ICallback callback) { - callbacks.add(callback); + public void addCallbackPush(ICallback callback) { + callbacksPush.add(callback); } - /** - * Synchronizes Edited, New and Deleted Notes. After all changed content has been sent to the - * server, it downloads all changes that happened on the server. + * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server. + * All callbacks will be executed once the synchronization operations are done. + * After execution the callback will be deleted, so it has to be added again if it shall be + * executed the next time all synchronize operations are finished. + * + * @param callback Implementation of ICallback, contains one method that shall be executed. */ - public void synchronize() { - uploadEditedNotes(); - uploadNewNotes(); - uploadDeletedNotes(); - downloadNotes(); + public void addCallbackPull(ICallback callback) { + callbacksPull.add(callback); } + /** - * Helper method to check if all synchronize operations are done yet. + * Schedules a synchronization and start it directly, if the network is connected and no + * synchronization is currently running. + * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server. */ - private void asyncTaskFinished() { - operationsFinished++; - if (operationsFinished == operationsCount) { - handler.obtainMessage(1).sendToTarget(); - } - } - - public void uploadEditedNotes() { - List notes = db.getNotesByStatus(DBStatus.LOCAL_EDITED); - for (Note note : notes) { - UploadEditedNotesTask editedNotesTask = new UploadEditedNotesTask(); - editedNotesTask.execute(note); - } - } - - public void uploadNewNotes() { - List notes = db.getNotesByStatus(DBStatus.LOCAL_CREATED); - for (Note note : notes) { - UploadNewNoteTask newNotesTask = new UploadNewNoteTask(); - newNotesTask.execute(note); - } - } - - public void uploadDeletedNotes() { - List notes = db.getNotesByStatus(DBStatus.LOCAL_DELETED); - for (Note note : notes) { - UploadDeletedNoteTask deletedNotesTask = new UploadDeletedNoteTask(); - deletedNotesTask.execute(note); + public void scheduleSync(boolean onlyLocalChanges) { + Log.d(getClass().getSimpleName(), "Sync requested ("+(onlyLocalChanges?"onlyLocalChanges":"full")+"; "+(networkConnected?"network connected":"network NOT connected")+", "+(syncActive?"sync active":"sync NOT active")+") ..."); + if(isSyncPossible() && (!syncActive || onlyLocalChanges)) { + Log.d(getClass().getSimpleName(), "... starting now"); + SyncTask syncTask = new SyncTask(onlyLocalChanges); + syncTask.addCallbacks(callbacksPush); + callbacksPush = new ArrayList<>(); + if(!onlyLocalChanges) { + syncTask.addCallbacks(callbacksPull); + callbacksPull = new ArrayList<>(); + } + syncTask.execute(); + } else if(!onlyLocalChanges) { + Log.d(getClass().getSimpleName(), "... scheduled"); + syncScheduled = true; + } else { + Log.d(getClass().getSimpleName(), "... do nothing"); } } - public void downloadNotes() { - DownloadNotesTask downloadNotesTask = new DownloadNotesTask(); - downloadNotesTask.execute(); - } + /** + * SyncTask is an AsyncTask which performs the synchronization in a background thread. + * Synchronization consists of two parts: pushLocalChanges and pullRemoteChanges. + */ + private class SyncTask extends AsyncTask { + private final boolean onlyLocalChanges; + private final List callbacks = new ArrayList<>(); + private NotesClient client; - private class UploadNewNoteTask extends AsyncTask { - @Override - protected Object[] doInBackground(Object... params) { - operationsCount++; - Note oldNote = (Note) params[0]; - try { - Note note = client.createNote(oldNote.getContent()); - return new Object[]{note, oldNote.getId()}; - } catch (IOException | JSONException e) { - e.printStackTrace(); - } - return null; + public SyncTask(boolean onlyLocalChanges) { + this.onlyLocalChanges = onlyLocalChanges; } - @Override - protected void onPostExecute(Object[] params) { - if (params != null) { - Long id = (Long) params[1]; - if (id != null) { - db.deleteNote(((Long) params[1])); - } - db.addNote((Note) params[0]); - } - asyncTaskFinished(); + public void addCallbacks(List callbacks) { + this.callbacks.addAll(callbacks); } - } - - private class UploadEditedNotesTask extends AsyncTask { - private String noteContent; @Override - protected Note doInBackground(Object... params) { - operationsCount++; - Note oldNote = (Note) params[0]; - noteContent = oldNote.getContent(); - try { - return client.editNote(oldNote.getId(), noteContent); - } catch (IOException | JSONException e) { - e.printStackTrace(); + protected void onPreExecute() { + super.onPreExecute(); + if(!onlyLocalChanges && syncScheduled) { + syncScheduled = false; } - return null; + syncActive = true; } @Override - protected void onPostExecute(Note note) { - if (note == null) { - // Note has been deleted on server -> recreate - db.addNoteAndSync(noteContent); - } else { - db.updateNote(note); + protected LoginStatus doInBackground(Void... voids) { + client = createNotesClient(); // recreate NoteClients on every sync in case the connection settings was changed + Log.d(getClass().getSimpleName(), "STARTING SYNCHRONIZATION"); + //dbHelper.debugPrintFullDB(); + LoginStatus status = LoginStatus.OK; + pushLocalChanges(); + if(!onlyLocalChanges) { + status = pullRemoteChanges(); } - asyncTaskFinished(); + //dbHelper.debugPrintFullDB(); + Log.d(getClass().getSimpleName(), "SYNCHRONIZATION FINISHED"); + return status; } - } - private class UploadDeletedNoteTask extends AsyncTask { - Long id = null; - - @Override - protected Void doInBackground(Object... params) { - operationsCount++; - try { - id = ((Note) params[0]).getId(); - client.deleteNote(id); - } catch (IOException e) { - e.printStackTrace(); + /** + * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server. + */ + private void pushLocalChanges() { + Log.d(getClass().getSimpleName(), "pushLocalChanges()"); + List notes = dbHelper.getLocalModifiedNotes(); + for (DBNote note : notes) { + Log.d(getClass().getSimpleName(), " Process Local Note: "+note); + try { + OwnCloudNote remoteNote=null; + switch(note.getStatus()) { + case LOCAL_EDITED: + Log.d(getClass().getSimpleName(), " ...create/edit"); + // if note is not new, try to edit it. + if (note.getRemoteId()>0) { + Log.d(getClass().getSimpleName(), " ...try to edit"); + remoteNote = client.editNote(note.getRemoteId(), note.getContent()); + } + // However, the note may be deleted on the server meanwhile; or was never synchronized -> (re)create + // Please note, thas dbHelper.updateNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI. + if (remoteNote == null) { + Log.d(getClass().getSimpleName(), " ...Note does not exist on server -> (re)create"); + remoteNote = client.createNote(note.getContent()); + dbHelper.updateNote(note.getId(), remoteNote, note); + } else { + dbHelper.updateNote(note.getId(), remoteNote, note); + } + break; + case LOCAL_DELETED: + if(note.getRemoteId()>0) { + Log.d(getClass().getSimpleName(), " ...delete (from server and local)"); + try { + client.deleteNote(note.getRemoteId()); + } catch (FileNotFoundException e) { + Log.d(getClass().getSimpleName(), " ...Note does not exist on server (anymore?) -> delete locally"); + } + } else { + Log.d(getClass().getSimpleName(), " ...delete (only local, since it was not synchronized)"); + } + // Please note, thas dbHelper.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI. + dbHelper.deleteNote(note.getId(), DBStatus.LOCAL_DELETED); + break; + default: + throw new IllegalStateException("Unknown State of Note: "+note); + } + } catch (IOException | JSONException e) { + // FIXME make some errors visible in the UI + e.printStackTrace(); + } } - return null; } - @Override - protected void onPostExecute(Void aVoid) { - db.deleteNote(id); - asyncTaskFinished(); - } - } - - private class DownloadNotesTask extends AsyncTask> { - private LoginStatus status = null; - - @Override - protected List doInBackground(Object... params) { - operationsCount++; - List notes = new ArrayList<>(); + /** + * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes. + */ + private LoginStatus pullRemoteChanges() { + Log.d(getClass().getSimpleName(), "pullRemoteChanges()"); + LoginStatus status = null; try { - notes = client.getNotes(); + List localNotes = dbHelper.getNotes(); + Map localIDmap = new HashMap<>(); + for (DBNote note : localNotes) { + localIDmap.put(note.getRemoteId(), note.getId()); + } + List remoteNotes = client.getNotes(); + Set remoteIDs = new HashSet<>(); + // pull remote changes: update or create each remote note + for (OwnCloudNote remoteNote : remoteNotes) { + Log.d(getClass().getSimpleName(), " Process Remote Note: "+remoteNote); + remoteIDs.add(remoteNote.getRemoteId()); + if(localIDmap.containsKey(remoteNote.getRemoteId())) { + Log.d(getClass().getSimpleName(), " ... found -> Update"); + dbHelper.updateNote(localIDmap.get(remoteNote.getRemoteId()), remoteNote, null); + } else { + Log.d(getClass().getSimpleName(), " ... create"); + dbHelper.addNote(remoteNote); + } + } + Log.d(getClass().getSimpleName(), " Remove remotely deleted Notes (only those without local changes)"); + // remove remotely deleted notes (only those without local changes) + for (DBNote note : localNotes) { + if(note.getStatus()==DBStatus.VOID && !remoteIDs.contains(note.getRemoteId())) { + Log.d(getClass().getSimpleName(), " ... remove "+note); + dbHelper.deleteNote(note.getId(), DBStatus.VOID); + } + } status = LoginStatus.OK; } catch (IOException e) { - e.printStackTrace(); status = LoginStatus.CONNECTION_FAILED; } catch (JSONException e) { status = LoginStatus.JSON_FAILED; } - return notes; + return status; } @Override - protected void onPostExecute(List result) { - // Clear Database only if there was no Server Error - if (status==LoginStatus.OK) { - db.clearDatabase(); - } else { - Context c = db.getContext(); - Toast.makeText(c, c.getString(R.string.error_sync, c.getString(status.str)), Toast.LENGTH_LONG).show(); + protected void onPostExecute(LoginStatus status) { + super.onPostExecute(status); + if (status!=LoginStatus.OK) { + Toast.makeText(appContext, appContext.getString(R.string.error_sync, appContext.getString(status.str)), Toast.LENGTH_LONG).show(); + } + syncActive = false; + // notify callbacks + for (ICallback callback : callbacks) { + callback.onFinish(); } - for (Note note : result) { - db.addNote(note); + // start next sync if scheduled meanwhile + if(syncScheduled) { + scheduleSync(false); } - asyncTaskFinished(); } } + + private NotesClient createNotesClient() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(appContext.getApplicationContext()); + String url = preferences.getString(SettingsActivity.SETTINGS_URL, SettingsActivity.DEFAULT_SETTINGS); + String username = preferences.getString(SettingsActivity.SETTINGS_USERNAME, SettingsActivity.DEFAULT_SETTINGS); + String password = preferences.getString(SettingsActivity.SETTINGS_PASSWORD, SettingsActivity.DEFAULT_SETTINGS); + return new NotesClient(url, username, password); + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClient.java index ac6d459ca..5a2db0d47 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClient.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClient.java @@ -18,7 +18,7 @@ import java.util.GregorianCalendar; import java.util.List; -import it.niedermann.owncloud.notes.model.Note; +import it.niedermann.owncloud.notes.model.OwnCloudNote; public class NotesClient { @@ -41,9 +41,9 @@ public NotesClient(String url, String username, String password) { this.password = password; } - public List getNotes() throws JSONException, + public List getNotes() throws JSONException, IOException { - List notesList = new ArrayList<>(); + List notesList = new ArrayList<>(); JSONArray notes = new JSONArray(requestServer("notes", METHOD_GET, null)); long noteId = 0; String noteTitle = ""; @@ -68,7 +68,7 @@ public List getNotes() throws JSONException, .setTimeInMillis(currentItem.getLong(key_modified) * 1000); } notesList - .add(new Note(noteId, noteModified, noteTitle, noteContent)); + .add(new OwnCloudNote(noteId, noteModified, noteTitle, noteContent)); } return notesList; } @@ -83,7 +83,7 @@ public List getNotes() throws JSONException, * @throws IOException */ @SuppressWarnings("unused") - public Note getNoteById(long id) throws + public OwnCloudNote getNoteById(long id) throws JSONException, IOException { long noteId = 0; String noteTitle = ""; @@ -106,7 +106,7 @@ public Note getNoteById(long id) throws noteModified .setTimeInMillis(currentItem.getLong(key_modified) * 1000); } - return new Note(noteId, noteModified, noteTitle, noteContent); + return new OwnCloudNote(noteId, noteModified, noteTitle, noteContent); } /** @@ -117,7 +117,7 @@ public Note getNoteById(long id) throws * @throws JSONException * @throws IOException */ - public Note createNote(String content) throws + public OwnCloudNote createNote(String content) throws JSONException, IOException { long noteId = 0; String noteTitle = ""; @@ -143,10 +143,10 @@ public Note createNote(String content) throws noteModified .setTimeInMillis(currentItem.getLong(key_modified) * 1000); } - return new Note(noteId, noteModified, noteTitle, noteContent); + return new OwnCloudNote(noteId, noteModified, noteTitle, noteContent); } - public Note editNote(long noteId, String content) + public OwnCloudNote editNote(long noteId, String content) throws JSONException, IOException { String noteTitle = ""; Calendar noteModified = null; @@ -164,7 +164,7 @@ public Note editNote(long noteId, String content) noteModified .setTimeInMillis(currentItem.getLong(key_modified) * 1000); } - return new Note(noteId, noteModified, noteTitle, content); + return new OwnCloudNote(noteId, noteModified, noteTitle, content); } public void deleteNote(long noteId) throws diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClientUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClientUtil.java index 16801f7c2..3bf521713 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClientUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesClientUtil.java @@ -25,6 +25,7 @@ public enum LoginStatus { OK(0), AUTH_FAILED(R.string.error_username_password_invalid), CONNECTION_FAILED(R.string.error_io), + NO_NETWORK(R.string.error_no_network), JSON_FAILED(R.string.error_json), SERVER_FAILED(R.string.error_server); diff --git a/app/src/main/res/drawable-hdpi/ic_sync_alert_black_18dp.png b/app/src/main/res/drawable-hdpi/ic_sync_alert_black_18dp.png new file mode 100644 index 000000000..d4f7994ef Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_sync_alert_black_18dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_sync_alert_black_18dp.png b/app/src/main/res/drawable-mdpi/ic_sync_alert_black_18dp.png new file mode 100644 index 000000000..b4947b753 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_sync_alert_black_18dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_sync_alert_black_18dp.png b/app/src/main/res/drawable-xhdpi/ic_sync_alert_black_18dp.png new file mode 100644 index 000000000..031a5c40c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_sync_alert_black_18dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_sync_alert_black_18dp.png b/app/src/main/res/drawable-xxhdpi/ic_sync_alert_black_18dp.png new file mode 100644 index 000000000..83cf8b06c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_sync_alert_black_18dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_sync_alert_black_18dp.png b/app/src/main/res/drawable-xxxhdpi/ic_sync_alert_black_18dp.png new file mode 100644 index 000000000..768785eac Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_sync_alert_black_18dp.png differ diff --git a/app/src/main/res/layout/fragment_notes_list_note_item.xml b/app/src/main/res/layout/fragment_notes_list_note_item.xml index 5a2c834bb..2c38a8a59 100644 --- a/app/src/main/res/layout/fragment_notes_list_note_item.xml +++ b/app/src/main/res/layout/fragment_notes_list_note_item.xml @@ -18,6 +18,7 @@ android:layout_alignParentEnd="false" android:layout_alignParentRight="true" android:layout_alignParentStart="true" + android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:ellipsize="end" @@ -38,6 +39,14 @@ android:textColor="@drawable/list_item_color_selector_low" android:textSize="14sp"/> + + Login fehlgeschlagen: %1$s ist die Server-App ownCloud Notes aktiviert? Server-Verbindung nicht möglich + keine Netzwerk-Verbindung URL/Server fehlerhaft URL nicht korrekt Benutzername / Passwort nicht korrekt diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 156d58905..59323823f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ Invalid login: %1$s is the owncloud Notes app activated on the server? server connection is broken + no network connection URL/Server has errors Wrong server address Wrong username or password