diff --git a/app/build.gradle b/app/build.gradle
index cf15cb573..578bdbaa9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -115,6 +115,7 @@ dependencies {
// ReactiveX
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
+ implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Testing
testImplementation 'androidx.test:core:1.5.0'
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
index b53a3b7bc..82cdf60f5 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
@@ -5,6 +5,7 @@
import android.app.Application;
import android.content.Context;
import android.util.Log;
+import android.webkit.WebView;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
@@ -29,6 +30,9 @@ public void onCreate() {
lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false);
isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false);
super.onCreate();
+ if (BuildConfig.DEBUG) {
+ WebView.setWebContentsDebuggingEnabled(true);
+ }
}
public static void setAppTheme(DarkModeSetting setting) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
index 4d969382a..162454336 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
@@ -71,7 +71,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
private Note originalNote;
private int originalScrollY;
protected NotesRepository repo;
- private NoteFragmentListener listener;
+ @Nullable
+ protected NoteFragmentListener listener;
private boolean titleModified = false;
protected boolean isNew = true;
@@ -143,6 +144,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
@Nullable
protected abstract ScrollView getScrollView();
+
protected abstract void scrollToY(int scrollY);
@Override
@@ -240,7 +242,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
.show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()));
return true;
} else if (itemId == R.id.menu_share) {
- ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
+ shareNote();
return false;
} else if (itemId == MENU_ID_PIN) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -263,6 +265,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
+ protected void shareNote() {
+ ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
+ }
+
@CallSuper
protected void onNoteLoaded(Note note) {
this.originalScrollY = note.getScrollY();
@@ -273,10 +279,21 @@ protected void onNoteLoaded(Note note) {
if (scrollY > 0) {
note.setScrollY(scrollY);
}
+ onScroll(scrollY, oldScrollY);
});
}
}
+ /**
+ * Scroll callback, to be overridden by subclasses. Default implementation is empty
+ */
+ protected void onScroll(int scrollY, int oldScrollY) {
+ }
+
+ protected boolean shouldShowToolbar() {
+ return true;
+ }
+
public void onCloseNote() {
if (!titleModified && originalNote == null && getContent().isEmpty()) {
repo.deleteNoteAndSync(localAccount, note.getId());
@@ -367,8 +384,14 @@ public void moveNote(Account account) {
}
public interface NoteFragmentListener {
+ enum Mode {
+ EDIT, PREVIEW, DIRECT_EDIT
+ }
+
void close();
void onNoteUpdated(Note note);
+
+ void changeMode(@NonNull Mode mode, boolean reloadNote);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
index 0fbc0bbf8..d5306d8ac 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
@@ -8,10 +8,12 @@
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
@@ -19,6 +21,7 @@
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.io.BufferedReader;
import java.io.IOException;
@@ -34,6 +37,7 @@
import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
import it.niedermann.owncloud.notes.main.MainActivity;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
@@ -57,11 +61,14 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
private ActivityEditBinding binding;
private BaseNoteFragment fragment;
+ private NotesRepository repo;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ repo = NotesRepository.getInstance(getApplicationContext());
+
try {
if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) == null) {
throw new NoCurrentAccountSelectedException();
@@ -118,9 +125,20 @@ private long getNoteId() {
}
private long getAccountId() {
- return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
+ final long idParam = getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
+ if (idParam == 0) {
+ try {
+ final SingleSignOnAccount ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(this);
+ return repo.getAccountByName(ssoAcc.name).getId();
+ } catch (NextcloudFilesAppAccountNotFoundException |
+ NoCurrentAccountSelectedException e) {
+ Log.w(TAG, "getAccountId: no current account", e);
+ }
+ }
+ return idParam;
}
+
/**
* Starts the note fragment for an existing note or a new note.
* The actual behavior is triggered by the activity's intent.
@@ -145,44 +163,109 @@ private void launchNoteFragment() {
* @param noteId ID of the existing note.
*/
private void launchExistingNote(long accountId, long noteId) {
- final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
- final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
- final var prefValueEdit = getString(R.string.pref_value_mode_edit);
- final var prefValuePreview = getString(R.string.pref_value_mode_preview);
- final var prefValueLast = getString(R.string.pref_value_mode_last);
+ launchExistingNote(accountId, noteId, null);
+ }
- final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- final String mode = preferences.getString(prefKeyNoteMode, prefValueEdit);
- final String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit);
- boolean editMode = true;
- if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) {
- editMode = false;
- }
- launchExistingNote(accountId, noteId, editMode);
+ private void launchExistingNote(long accountId, long noteId, @Nullable final String mode) {
+ launchExistingNote(accountId, noteId, mode, false);
}
/**
* Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note.
*
- * @param noteId ID of the existing note.
- * @param edit View-mode of the fragment:
- * true for {@link NoteEditFragment},
- * false for {@link NotePreviewFragment}.
+ * @param noteId ID of the existing note.
+ * @param mode View-mode of the fragment (pref value or null). If null will be chosen based on
+ * user preferences.
+ * @param discardState If true, the state of the fragment will be discarded and a new fragment will be created
*/
- private void launchExistingNote(long accountId, long noteId, boolean edit) {
+ private void launchExistingNote(long accountId, long noteId, @Nullable final String mode, final boolean discardState) {
// save state of the fragment in order to resume with the same note and originalNote
- Fragment.SavedState savedState = null;
- if (fragment != null) {
- savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
+ runOnUiThread(() -> {
+ Fragment.SavedState savedState = null;
+ if (fragment != null && !discardState) {
+ savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
+ }
+ fragment = getNoteFragment(accountId, noteId, mode);
+ if (savedState != null) {
+ fragment.setInitialSavedState(savedState);
+ }
+ replaceFragment();
+ });
+ }
+
+ private void replaceFragment() {
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ if (!fragment.shouldShowToolbar()) {
+ binding.toolbar.setVisibility(View.GONE);
+ } else {
+ binding.toolbar.setVisibility(View.VISIBLE);
}
- fragment = edit
- ? NoteEditFragment.newInstance(accountId, noteId)
- : NotePreviewFragment.newInstance(accountId, noteId);
+ }
+
+
+ /**
+ * Returns the preferred mode for the account. If the mode is "remember last" the last mode is returned.
+ * If the mode is "direct edit" and the account does not support direct edit, the default mode is returned.
+ */
+ private String getPreferenceMode(long accountId) {
+
+ final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
+ final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
+ final var defaultMode = getString(R.string.pref_value_mode_edit);
+ final var prefValueLast = getString(R.string.pref_value_mode_last);
+ final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
- if (savedState != null) {
- fragment.setInitialSavedState(savedState);
+
+ final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final String modePreference = preferences.getString(prefKeyNoteMode, defaultMode);
+
+ String effectiveMode = modePreference;
+ if (modePreference.equals(prefValueLast)) {
+ effectiveMode = preferences.getString(prefKeyLastMode, defaultMode);
+ }
+
+ if (effectiveMode.equals(prefValueDirectEdit)) {
+ final Account accountById = repo.getAccountById(accountId);
+ final var directEditAvailable = accountById != null && accountById.isDirectEditingAvailable();
+ if (!directEditAvailable) {
+ effectiveMode = defaultMode;
+ }
+ }
+
+ return effectiveMode;
+ }
+
+ private BaseNoteFragment getNoteFragment(long accountId, long noteId, final @Nullable String modePref) {
+
+ final var effectiveMode = modePref == null ? getPreferenceMode(accountId) : modePref;
+
+ final var prefValueEdit = getString(R.string.pref_value_mode_edit);
+ final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
+ final var prefValuePreview = getString(R.string.pref_value_mode_preview);
+
+ if (effectiveMode.equals(prefValueEdit)) {
+ return NoteEditFragment.newInstance(accountId, noteId);
+ } else if (effectiveMode.equals(prefValueDirectEdit)) {
+ return NoteDirectEditFragment.newInstance(accountId, noteId);
+ } else if (effectiveMode.equals(prefValuePreview)) {
+ return NotePreviewFragment.newInstance(accountId, noteId);
+ } else {
+ throw new IllegalStateException("Unknown note modePref: " + modePref);
+ }
+ }
+
+
+ @NonNull
+ private BaseNoteFragment getNewNoteFragment(Note newNote) {
+ final var mode = getPreferenceMode(getAccountId());
+
+ final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
+
+ if (mode.equals(prefValueDirectEdit)) {
+ return NoteDirectEditFragment.newInstanceWithNewNote(newNote);
+ } else {
+ return NoteEditFragment.newInstanceWithNewNote(newNote);
}
- getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
}
/**
@@ -219,10 +302,11 @@ private void launchNewNote() {
content = "";
}
final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
- fragment = NoteEditFragment.newInstanceWithNewNote(newNote);
- getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ fragment = getNewNoteFragment(newNote);
+ replaceFragment();
}
+
private void launchReadonlyNote() {
final var intent = getIntent();
final var content = new StringBuilder();
@@ -238,7 +322,7 @@ private void launchReadonlyNote() {
}
fragment = NoteReadonlyFragment.newInstance(content.toString());
- getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ replaceFragment();
}
@Override
@@ -260,10 +344,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
close();
return true;
} else if (itemId == R.id.menu_preview) {
- launchExistingNote(getAccountId(), getNoteId(), false);
+ changeMode(Mode.PREVIEW, false);
return true;
} else if (itemId == R.id.menu_edit) {
- launchExistingNote(getAccountId(), getNoteId(), true);
+ changeMode(Mode.EDIT, false);
return true;
}
return super.onOptionsItemSelected(item);
@@ -281,8 +365,10 @@ public void close() {
final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
if (fragment instanceof NoteEditFragment) {
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply();
- } else {
+ } else if (fragment instanceof NotePreviewFragment) {
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply();
+ } else if (fragment instanceof NoteDirectEditFragment) {
+ preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_direct_edit)).apply();
}
fragment.onCloseNote();
@@ -308,6 +394,24 @@ public void onNoteUpdated(Note note) {
}
}
+ @Override
+ public void changeMode(@NonNull Mode mode, boolean reloadNote) {
+ switch (mode) {
+ case EDIT:
+ launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_edit), reloadNote);
+ break;
+ case PREVIEW:
+ launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_preview), reloadNote);
+ break;
+ case DIRECT_EDIT:
+ launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_direct_edit), reloadNote);
+ break;
+ default:
+ throw new IllegalStateException("Unknown mode: " + mode);
+ }
+ }
+
+
@Override
public void onAccountPicked(@NonNull Account account) {
fragment.moveNote(account);
@@ -318,4 +422,4 @@ public void applyBrand(int color) {
final var util = BrandingUtil.of(color, this);
util.notes.applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar, colorAccent);
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteDirectEditFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteDirectEditFragment.kt
new file mode 100644
index 000000000..0c9b9772f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteDirectEditFragment.kt
@@ -0,0 +1,401 @@
+package it.niedermann.owncloud.notes.edit
+
+import android.annotation.SuppressLint
+import android.net.http.SslError
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.JavascriptInterface
+import android.webkit.SslErrorHandler
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.ScrollView
+import androidx.core.view.isVisible
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.android.sso.helper.SingleAccountHelper
+import com.nextcloud.android.sso.model.SingleSignOnAccount
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import it.niedermann.owncloud.notes.BuildConfig
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.Branded
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import it.niedermann.owncloud.notes.databinding.FragmentNoteDirectEditBinding
+import it.niedermann.owncloud.notes.persistence.ApiProvider
+import it.niedermann.owncloud.notes.persistence.DirectEditingRepository
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.persistence.sync.NotesAPI
+import it.niedermann.owncloud.notes.shared.model.ApiVersion
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback
+import it.niedermann.owncloud.notes.shared.util.ExtendedFabUtil
+import it.niedermann.owncloud.notes.shared.util.rx.DisposableSet
+import java.util.concurrent.TimeUnit
+
+class NoteDirectEditFragment : BaseNoteFragment(), Branded {
+ private var _binding: FragmentNoteDirectEditBinding? = null
+ private val binding: FragmentNoteDirectEditBinding
+ get() = _binding!!
+
+ private val disposables: DisposableSet = DisposableSet()
+ private var switchToEditPending = false
+
+ val account: SingleSignOnAccount by lazy {
+ SingleAccountHelper.getCurrentSingleSignOnAccount(
+ requireContext(),
+ )
+ }
+
+ val notesApi: NotesAPI by lazy {
+ ApiProvider.getInstance().getNotesAPI(requireContext(), account, ApiVersion.API_VERSION_1_0)
+ }
+
+ // for hiding / showing the fab
+ private var scrollStart: Int = 0
+
+ public override fun getScrollView(): ScrollView? {
+ return null
+ }
+
+ override fun scrollToY(y: Int) {
+ // do nothing
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ Log.d(TAG, "onCreateView() called")
+ _binding = FragmentNoteDirectEditBinding.inflate(inflater, container, false)
+ setupFab()
+ prepareWebView()
+ return binding.root
+ }
+
+ @SuppressLint("ClickableViewAccessibility") // touch listener only for UI purposes, no need to handle click
+ private fun setupFab() {
+ binding.plainEditingFab.isExtended = false
+ ExtendedFabUtil.toggleExtendedOnLongClick(binding.plainEditingFab)
+ // manually detect scroll as we can't get it from the webview (maybe with custom JS?)
+ binding.noteWebview.setOnTouchListener { _, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ scrollStart = event.y.toInt()
+ }
+ MotionEvent.ACTION_UP -> {
+ val scrollEnd = event.y.toInt()
+ ExtendedFabUtil.toggleVisibilityOnScroll(
+ binding.plainEditingFab,
+ scrollStart,
+ scrollEnd,
+ )
+ }
+ }
+ return@setOnTouchListener false
+ }
+ binding.plainEditingFab.setOnClickListener { switchToPlainEdit() }
+ }
+
+ private fun switchToPlainEdit() {
+ switchToEditPending = true
+ binding.noteWebview.evaluateJavascript(JS_CLOSE) { result ->
+ val resultWithoutQuotes = result.replace("\"", "")
+ if (resultWithoutQuotes != JS_RESULT_OK) {
+ Log.w(TAG, "Closing via JS failed: $resultWithoutQuotes")
+ changeToEditMode()
+ }
+ // if result is OK, switch will be handled by JS interface callback
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ disposables.dispose()
+ binding.noteWebview.destroy()
+ _binding = null
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val timeoutDisposable = Single.just(Unit)
+ .delay(LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .map {
+ if (!binding.noteWebview.isVisible) {
+ Log.w(TAG, "Editor not loaded after $LOAD_TIMEOUT_SECONDS seconds")
+ handleLoadError()
+ }
+ }.subscribe()
+ disposables.add(timeoutDisposable)
+ }
+
+ override fun onNoteLoaded(note: Note) {
+ super.onNoteLoaded(note)
+ Log.d(TAG, "onNoteLoaded() called")
+ val newNoteParam = arguments?.getSerializable(PARAM_NEWNOTE) as Note?
+ if (newNoteParam != null || note.remoteId == null) {
+ createAndLoadNote(note)
+ } else {
+ loadNoteInWebView(note)
+ }
+ }
+
+ private fun createAndLoadNote(newNote: Note) {
+ Log.d(TAG, "createAndLoadNote() called")
+ val noteCreateDisposable = Single
+ .fromCallable {
+ notesApi.createNote(newNote).execute().body()!!
+ }
+ .map { createdNote ->
+ repo.updateRemoteId(newNote.id, createdNote.remoteId)
+ repo.getNoteById(newNote.id)
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ createdNote ->
+ loadNoteInWebView(createdNote)
+ }, { throwable ->
+ note = null
+ handleLoadError()
+ Log.e(TAG, "createAndLoadNote:", throwable)
+ })
+ disposables.add(noteCreateDisposable)
+ }
+
+ private fun loadNoteInWebView(note: Note) {
+ Log.d(TAG, "loadNoteInWebView() called")
+ val directEditingRepository =
+ DirectEditingRepository.getInstance(requireContext().applicationContext)
+ val urlDisposable = directEditingRepository.getDirectEditingUrl(account, note)
+ .observeOn(AndroidSchedulers.mainThread()).subscribe({ url ->
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "loadNoteInWebView: url = $url")
+ }
+ binding.noteWebview.loadUrl(url)
+ }, { throwable ->
+ handleLoadError()
+ Log.e(TAG, "loadNoteInWebView:", throwable)
+ })
+ disposables.add(urlDisposable)
+ }
+
+ private fun handleLoadError() {
+ val snackbar = BrandedSnackbar.make(
+ binding.plainEditingFab,
+ getString(R.string.direct_editing_error),
+ Snackbar.LENGTH_INDEFINITE,
+ )
+ if (note != null) {
+ snackbar.setAction(R.string.switch_to_plain_editing) {
+ changeToEditMode()
+ }
+ } else {
+ snackbar.setAction(R.string.action_back) {
+ close()
+ }
+ }
+ snackbar.show()
+ }
+
+ override fun shouldShowToolbar(): Boolean = false
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun prepareWebView() {
+ val webSettings = binding.noteWebview.settings
+ // enable zoom
+ webSettings.setSupportZoom(true)
+ webSettings.builtInZoomControls = true
+ webSettings.displayZoomControls = false
+
+ // Non-responsive webs are zoomed out when loaded
+ webSettings.useWideViewPort = true
+ webSettings.loadWithOverviewMode = true
+
+ // user agent
+ val userAgent =
+ getString(R.string.user_agent, getString(R.string.app_name), BuildConfig.VERSION_NAME)
+ webSettings.userAgentString = userAgent
+
+ // no private data storing
+ webSettings.savePassword = false
+ webSettings.saveFormData = false
+
+ // disable local file access
+ webSettings.allowFileAccess = false
+
+ // enable javascript
+ webSettings.javaScriptEnabled = true
+ webSettings.domStorageEnabled = true
+
+ if (BuildConfig.DEBUG) {
+ // caching disabled in debug mode
+ binding.noteWebview.settings.cacheMode = WebSettings.LOAD_NO_CACHE
+ }
+
+ binding.noteWebview.addJavascriptInterface(
+ DirectEditingMobileInterface(this),
+ JS_INTERFACE_NAME,
+ )
+
+ binding.noteWebview.webViewClient = object : WebViewClient() {
+ override fun onReceivedError(
+ view: WebView?,
+ request: WebResourceRequest?,
+ error: WebResourceError?,
+ ) {
+ super.onReceivedError(view, request, error)
+ if (request?.isForMainFrame == true) {
+ handleLoadError()
+ }
+ }
+
+ @SuppressLint("WebViewClientOnReceivedSslError") // only for debug mode
+ override fun onReceivedSslError(
+ view: WebView?,
+ handler: SslErrorHandler?,
+ error: SslError?,
+ ) {
+ if (BuildConfig.DEBUG) {
+ handler?.proceed()
+ } else {
+ super.onReceivedSslError(view, handler, error)
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the current content of the EditText field in the UI.
+ *
+ * @return String of the current content.
+ */
+ override fun getContent(): String {
+ // no way to get content from webview
+ return ""
+ }
+
+ override fun saveNote(callback: ISyncCallback?) {
+ val acc = repo.getAccountByName(account.name)
+ repo.scheduleSync(acc, false)
+ }
+
+ override fun onCloseNote() {
+ saveNote(null)
+ }
+
+ override fun applyBrand(color: Int) {
+ val util = BrandingUtil.of(color, requireContext())
+ util.material.themeExtendedFAB(binding.plainEditingFab)
+ util.platform.colorCircularProgressBar(binding.progress, ColorRole.PRIMARY)
+ }
+
+ private class DirectEditingMobileInterface(val noteDirectEditFragment: NoteDirectEditFragment) {
+ @JavascriptInterface
+ fun close() {
+ noteDirectEditFragment.close()
+ }
+
+ @JavascriptInterface
+ fun share() {
+ noteDirectEditFragment.share()
+ }
+
+ @JavascriptInterface
+ fun loaded() {
+ noteDirectEditFragment.onLoaded()
+ }
+ }
+
+ private fun close() {
+ if (switchToEditPending) {
+ Log.d(TAG, "close: switching to plain edit")
+ changeToEditMode()
+ } else {
+ Log.d(TAG, "close: closing")
+ listener?.close()
+ }
+ }
+
+ private fun changeToEditMode() {
+ toggleLoadingUI(true)
+ val updateDisposable = Single.just(note.remoteId)
+ .map { remoteId ->
+ val newNote = notesApi.getNote(remoteId).singleOrError().blockingGet().response
+ val localAccount = repo.getAccountByName(account.name)
+ repo.updateNoteAndSync(localAccount, note, newNote.content, newNote.title, null)
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({
+ listener?.changeMode(NoteFragmentListener.Mode.EDIT, true)
+ }, { throwable ->
+ Log.e(TAG, "changeToEditMode: ", throwable)
+ listener?.changeMode(NoteFragmentListener.Mode.EDIT, true)
+ })
+ disposables.add(updateDisposable)
+ }
+
+ private fun share() {
+ super.shareNote()
+ }
+
+ private fun onLoaded() {
+ Log.d(TAG, "onLoaded: note loaded")
+ toggleLoadingUI(false)
+ }
+
+ private fun toggleLoadingUI(loading: Boolean) {
+ activity?.runOnUiThread {
+ binding.progress.isVisible = loading
+ binding.noteWebview.isVisible = !loading
+ binding.plainEditingFab.isVisible = !loading
+ }
+ }
+
+ companion object {
+ private const val TAG = "NoteDirectEditFragment"
+ private const val LOAD_TIMEOUT_SECONDS = 10L
+ private const val JS_INTERFACE_NAME = "DirectEditingMobileInterface"
+ private const val JS_RESULT_OK = "ok"
+
+ // language=js
+ private val JS_CLOSE = """
+ (function () {
+ var closeIcons = document.getElementsByClassName("icon-close");
+ if (closeIcons.length > 0) {
+ closeIcons[0].click();
+ } else {
+ return "close button not available";
+ }
+ return "$JS_RESULT_OK";
+ })();
+ """.trimIndent()
+
+ @JvmStatic
+ fun newInstance(accountId: Long, noteId: Long): BaseNoteFragment {
+ val fragment = NoteDirectEditFragment()
+ val args = Bundle()
+ args.putLong(PARAM_NOTE_ID, noteId)
+ args.putLong(PARAM_ACCOUNT_ID, accountId)
+ fragment.arguments = args
+ return fragment
+ }
+
+ @JvmStatic
+ fun newInstanceWithNewNote(newNote: Note?): BaseNoteFragment {
+ val fragment = NoteDirectEditFragment()
+ val args = Bundle()
+ args.putSerializable(PARAM_NEWNOTE, newNote)
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
index 13061417c..9ba17f95a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
@@ -25,6 +25,7 @@
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.niedermann.owncloud.notes.R;
@@ -103,6 +104,11 @@ protected FloatingActionButton getSearchPrevButton() {
return binding.searchPrev;
}
+ @Override
+ protected @NonNull ExtendedFloatingActionButton getDirectEditingButton() {
+ return binding.directEditing;
+ }
+
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
index 0b2991a7d..37048d6ac 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
@@ -23,6 +23,7 @@
import androidx.preference.PreferenceManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
@@ -80,6 +81,11 @@ protected FloatingActionButton getSearchPrevButton() {
return binding.searchPrev;
}
+ @Override
+ protected @NonNull ExtendedFloatingActionButton getDirectEditingButton() {
+ return binding.directEditing;
+ }
+
@Override
protected Layout getLayout() {
binding.singleNoteContent.onPreDraw();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java
index 4d1ec24fd..10c9510cb 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java
@@ -1,6 +1,5 @@
package it.niedermann.owncloud.notes.edit;
-import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.text.Layout;
@@ -16,12 +15,19 @@
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.regex.Pattern;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.util.ExtendedFabUtil;
public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
@@ -34,6 +40,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
private SearchView searchView;
private String searchQuery = null;
private static final int delay = 50; // If the search string does not change after $delay ms, then the search task starts.
+ private boolean directEditAvailable = false;
@ColorInt
private int color;
@@ -54,6 +61,47 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
}
}
+ @Override
+ protected void onScroll(int scrollY, int oldScrollY) {
+ super.onScroll(scrollY, oldScrollY);
+ if (directEditAvailable) {
+ // only show FAB if search is not active
+ if (getSearchNextButton() == null || getSearchNextButton().getVisibility() != View.VISIBLE) {
+ final ExtendedFloatingActionButton directFab = getDirectEditingButton();
+ ExtendedFabUtil.toggleVisibilityOnScroll(directFab, scrollY, oldScrollY);
+ }
+ }
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ checkDirectEditingAvailable();
+ if (directEditAvailable) {
+ final ExtendedFloatingActionButton directEditingButton = getDirectEditingButton();
+ directEditingButton.setExtended(false);
+ ExtendedFabUtil.toggleExtendedOnLongClick(directEditingButton);
+ directEditingButton.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.changeMode(NoteFragmentListener.Mode.DIRECT_EDIT, false);
+ }
+ });
+ } else {
+ getDirectEditingButton().setVisibility(View.GONE);
+ }
+ }
+
+ private void checkDirectEditingAvailable() {
+ try {
+ final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext());
+ final Account localAccount = repo.getAccountByName(ssoAccount.name);
+ directEditAvailable = localAccount != null && localAccount.isDirectEditingAvailable();
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ Log.w(TAG, "checkDirectEditingAvailable: ", e);
+ directEditAvailable = false;
+ }
+ }
+
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
@@ -199,7 +247,12 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
protected abstract FloatingActionButton getSearchPrevButton();
+ @NonNull
+ protected abstract ExtendedFloatingActionButton getDirectEditingButton();
+
+
private void showSearchFabs() {
+ ExtendedFabUtil.setExtendedFabVisibility(getDirectEditingButton(), false);
final var next = getSearchNextButton();
final var prev = getSearchPrevButton();
if (prev != null) {
@@ -291,5 +344,6 @@ public void applyBrand(int color) {
final var util = BrandingUtil.of(color, requireContext());
util.material.themeFAB(getSearchNextButton());
util.material.themeFAB(getSearchPrevButton());
+ util.material.themeExtendedFAB(getDirectEditingButton());
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
index 3ec92ffb5..479529f30 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
@@ -10,14 +10,12 @@
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
-import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
import static it.niedermann.owncloud.notes.shared.util.SSOUtil.askForNewAccount;
import android.accounts.NetworkErrorException;
import android.animation.AnimatorInflater;
import android.app.SearchManager;
import android.content.Intent;
-import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
@@ -50,7 +48,6 @@
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
-import com.nextcloud.android.common.ui.util.PlatformThemeUtil;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
index 30e365e2d..75d02c52d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -414,6 +414,7 @@ public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IRes
localAccount.setColor(capabilities.getColor());
BrandingUtil.saveBrandColor(getApplication(), localAccount.getColor());
repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
+ repo.updateDirectEditingAvailable(localAccount.getId(), capabilities.isDirectEditingAvailable());
callback.onSuccess(null);
} catch (Throwable t) {
if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) {
@@ -662,4 +663,4 @@ private boolean isNetworkUnreachable(@Nullable String input) {
final var lower = input.toLowerCase(Locale.ROOT);
return lower.contains("failed to connect") && lower.contains("network is unreachable");
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
index ce6096075..4be0a493d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
@@ -20,6 +20,7 @@
import java.util.concurrent.ConcurrentHashMap;
import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer;
+import it.niedermann.owncloud.notes.persistence.sync.FilesAPI;
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
@@ -39,11 +40,14 @@ public class ApiProvider {
private static final ApiProvider INSTANCE = new ApiProvider();
private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
+ private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/";
private static final Map API_CACHE = new ConcurrentHashMap<>();
private static final Map API_CACHE_OCS = new ConcurrentHashMap<>();
private static final Map API_CACHE_NOTES = new ConcurrentHashMap<>();
+ private static final Map API_CACHE_FILES = new ConcurrentHashMap<>();
+
public static ApiProvider getInstance() {
return INSTANCE;
@@ -77,6 +81,15 @@ public synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull Sing
return notesAPI;
}
+ public synchronized FilesAPI getFilesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ if (API_CACHE_FILES.containsKey(ssoAccount.name)) {
+ return API_CACHE_FILES.get(ssoAccount.name);
+ }
+ final var filesAPI = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_FILES).create(FilesAPI.class);
+ API_CACHE_FILES.put(ssoAccount.name, filesAPI);
+ return filesAPI;
+ }
+
private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
if (API_CACHE.containsKey(ssoAccount.name)) {
return API_CACHE.get(ssoAccount.name);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
index ebe6198ed..43ff28812 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
@@ -51,6 +51,7 @@ public Result doWork() {
repo.updateCapabilitiesETag(account.getId(), capabilities.getETag());
repo.updateBrand(account.getId(), capabilities.getColor());
repo.updateApiVersion(account.getId(), capabilities.getApiVersion());
+ repo.updateDirectEditingAvailable(account.getId(), capabilities.isDirectEditingAvailable());
Log.i(TAG, capabilities.toString());
repo.updateDisplayName(account.getId(), CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()));
} catch (Throwable e) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/DirectEditingRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/DirectEditingRepository.kt
new file mode 100644
index 000000000..01cdfb1ac
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/DirectEditingRepository.kt
@@ -0,0 +1,69 @@
+package it.niedermann.owncloud.notes.persistence
+
+import android.app.Application
+import android.content.Context
+import com.nextcloud.android.sso.model.SingleSignOnAccount
+import io.reactivex.Single
+import io.reactivex.schedulers.Schedulers
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.shared.model.ApiVersion
+import it.niedermann.owncloud.notes.shared.model.directediting.DirectEditingRequestBody
+
+class DirectEditingRepository private constructor(private val applicationContext: Context) {
+
+ private val apiProvider: ApiProvider by lazy { ApiProvider.getInstance() }
+ private val notesRepository: NotesRepository by lazy {
+ NotesRepository.getInstance(
+ applicationContext,
+ )
+ }
+
+ private fun getNotesPath(account: SingleSignOnAccount): Single {
+ return Single.fromCallable {
+ val call = notesRepository.getServerSettings(account, ApiVersion.API_VERSION_1_0)
+ val response = call.execute()
+ response.body()?.notesPath ?: throw RuntimeException("No notes path available")
+ }.subscribeOn(Schedulers.io())
+ }
+
+ fun getDirectEditingUrl(
+ account: SingleSignOnAccount,
+ note: Note,
+ ): Single {
+ return getNotesPath(account)
+ .flatMap { notesPath ->
+ val filesAPI = apiProvider.getFilesAPI(applicationContext, account)
+ Single.fromCallable {
+ val call =
+ filesAPI.getDirectEditingUrl(
+ DirectEditingRequestBody(
+ path = notesPath,
+ editorId = SUPPORTED_EDITOR_ID,
+ fileId = note.remoteId!!,
+ ),
+ )
+ val response = call.execute()
+ response.body()?.ocs?.data?.url
+ ?: throw RuntimeException("No url available")
+ }.subscribeOn(Schedulers.io())
+ }
+ }
+
+ companion object {
+ private const val SUPPORTED_EDITOR_ID = "text"
+
+ private var instance: DirectEditingRepository? = null
+
+ /**
+ * @param applicationContext The application context. Do NOT use a view context to prevent leaks.
+ */
+ @JvmStatic
+ fun getInstance(applicationContext: Context): DirectEditingRepository {
+ require(applicationContext is Application)
+ if (instance == null) {
+ instance = DirectEditingRepository(applicationContext)
+ }
+ return instance!!
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
index d062f4c54..a50604073 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
@@ -21,22 +21,7 @@
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_10_11;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_11_12;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_12_13;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_13_14;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_14_15;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_15_16;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_16_17;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_17_18;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_18_19;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_19_20;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_20_21;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_21_22;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_22_23;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_23_24;
-import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
-
+import it.niedermann.owncloud.notes.persistence.migration.*;
@Database(
entities = {
Account.class,
@@ -44,7 +29,7 @@
CategoryOptions.class,
SingleNoteWidgetData.class,
NotesListWidgetData.class
- }, version = 24
+ }, version = 25
)
@TypeConverters({Converters.class})
public abstract class NotesDatabase extends RoomDatabase {
@@ -80,7 +65,8 @@ private static NotesDatabase create(final Context context) {
new Migration_20_21(),
new Migration_21_22(context),
new Migration_22_23(),
- new Migration_23_24(context)
+ new Migration_23_24(context),
+ new Migration_24_25()
)
.fallbackToDestructiveMigrationOnDowngrade()
.fallbackToDestructiveMigration()
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
index 6825fdba3..1ea2f7cbf 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
@@ -263,6 +263,9 @@ public void updateCapabilitiesETag(long id, String capabilitiesETag) {
public void updateModified(long id, long modified) {
db.getAccountDao().updateModified(id, modified);
}
+ public void updateDirectEditingAvailable(final long id, final boolean available) {
+ db.getAccountDao().updateDirectEditingAvailable(id, available);
+ }
// Notes
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
index bb4a05410..8b3424adf 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
@@ -21,8 +21,8 @@ public interface AccountDao {
@Delete
void deleteAccount(Account localAccount);
- String getAccounts = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account";
- String getAccountById = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ID = :accountId";
+ String getAccounts = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName, directEditingAvailable FROM Account";
+ String getAccountById = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName, directEditingAvailable FROM Account WHERE ID = :accountId";
@Query(getAccounts)
LiveData> getAccounts$();
@@ -36,7 +36,7 @@ public interface AccountDao {
@Query(getAccountById)
Account getAccountById(long accountId);
- @Query("SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ACCOUNTNAME = :accountName")
+ @Query("SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName, directEditingAvailable FROM Account WHERE ACCOUNTNAME = :accountName")
Account getAccountByName(String accountName);
@Query("SELECT COUNT(*) FROM Account")
@@ -59,4 +59,7 @@ public interface AccountDao {
@Query("UPDATE Account SET DISPLAYNAME = :displayName WHERE id = :id")
void updateDisplayName(long id, @Nullable String displayName);
+
+ @Query("UPDATE Account SET directEditingAvailable = :available WHERE id = :id")
+ void updateDirectEditingAvailable(long id, boolean available);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
index 24e5222d8..05b957f0f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
@@ -10,17 +10,9 @@
import androidx.room.Index;
import androidx.room.PrimaryKey;
-import org.json.JSONArray;
-import org.json.JSONException;
-
import java.io.Serializable;
import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.NoSuchElementException;
-import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@Entity(
@@ -60,6 +52,7 @@ public class Account implements Serializable {
private String capabilitiesETag;
@Nullable
private String displayName;
+ private boolean directEditingAvailable;
public Account() {
// Default constructor
@@ -76,6 +69,7 @@ public Account(@NonNull String url, @NonNull String username, @NonNull String ac
public void setCapabilities(@NonNull Capabilities capabilities) {
capabilitiesETag = capabilities.getETag();
apiVersion = capabilities.getApiVersion();
+ directEditingAvailable = capabilities.isDirectEditingAvailable();
setColor(capabilities.getColor());
}
@@ -175,6 +169,14 @@ public void setDisplayName(@Nullable String displayName) {
this.displayName = displayName;
}
+ public boolean isDirectEditingAvailable() {
+ return directEditingAvailable;
+ }
+
+ public void setDirectEditingAvailable(boolean directEditingAvailable) {
+ this.directEditingAvailable = directEditingAvailable;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -195,6 +197,7 @@ public boolean equals(Object o) {
return false;
if (capabilitiesETag != null ? !capabilitiesETag.equals(account.capabilitiesETag) : account.capabilitiesETag != null)
return false;
+ if (directEditingAvailable != account.directEditingAvailable) return false;
return true;
}
@@ -210,6 +213,7 @@ public int hashCode() {
result = 31 * result + color;
result = 31 * result + textColor;
result = 31 * result + (capabilitiesETag != null ? capabilitiesETag.hashCode() : 0);
+ result = 31 * result + (directEditingAvailable ? 1 : 0);
return result;
}
@@ -227,6 +231,7 @@ public String toString() {
", color=" + color +
", textColor=" + textColor +
", capabilitiesETag='" + capabilitiesETag + '\'' +
+ ", directEditingAvailable='" + directEditingAvailable + '\'' +
'}';
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_24_25.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_24_25.kt
new file mode 100644
index 000000000..e172d1786
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_24_25.kt
@@ -0,0 +1,13 @@
+package it.niedermann.owncloud.notes.persistence.migration
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+@Suppress("ClassName", "Detekt.ClassNaming", "Detekt.MagicNumber")
+class Migration_24_25 : Migration(24, 25) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE Account ADD COLUMN directEditingAvailable INTEGER DEFAULT 0 NOT NULL")
+ // remove capabilities etag to force refresh
+ db.execSQL("UPDATE Account SET capabilitiesETag = NULL")
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
index d5ae7b494..e3b268ab7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
@@ -31,6 +31,9 @@ public class CapabilitiesDeserializer implements JsonDeserializer
private static final String CAPABILITIES_THEMING = "theming";
private static final String CAPABILITIES_THEMING_COLOR = "color";
private static final String CAPABILITIES_THEMING_COLOR_TEXT = "color-text";
+ private static final String CAPABILITIES_FILES = "files";
+ private static final String CAPABILITIES_FILES_DIRECT_EDITING = "directEditing";
+ private static final String CAPABILITIES_FILES_DIRECT_EDITING_SUPPORTS_FILE_ID = "supportsFileId";
@Override
public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
@@ -61,7 +64,21 @@ public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializat
}
}
}
+ response.setDirectEditingAvailable(hasDirectEditingCapability(capabilities));
}
return response;
}
+
+ private boolean hasDirectEditingCapability(final JsonObject capabilities) {
+ if (capabilities.has(CAPABILITIES_FILES)) {
+ final var files = capabilities.getAsJsonObject(CAPABILITIES_FILES);
+ if (files.has(CAPABILITIES_FILES_DIRECT_EDITING)) {
+ final var directEditing = files.getAsJsonObject(CAPABILITIES_FILES_DIRECT_EDITING);
+ if (directEditing.has(CAPABILITIES_FILES_DIRECT_EDITING_SUPPORTS_FILE_ID)) {
+ return directEditing.get(CAPABILITIES_FILES_DIRECT_EDITING_SUPPORTS_FILE_ID).getAsBoolean();
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/FilesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/FilesAPI.kt
new file mode 100644
index 000000000..a14dcdd84
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/FilesAPI.kt
@@ -0,0 +1,18 @@
+package it.niedermann.owncloud.notes.persistence.sync
+
+import it.niedermann.owncloud.notes.shared.model.OcsResponse
+import it.niedermann.owncloud.notes.shared.model.OcsUrl
+import it.niedermann.owncloud.notes.shared.model.directediting.DirectEditingInfo
+import it.niedermann.owncloud.notes.shared.model.directediting.DirectEditingRequestBody
+import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+
+interface FilesAPI {
+ @GET("directEditing?format=json")
+ fun getDirectEditingInfo(): Call>
+
+ @POST("directEditing/open?format=json")
+ fun getDirectEditingUrl(@Body body: DirectEditingRequestBody): Call>
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
index 06bd867d3..e3d739dee 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
@@ -3,7 +3,6 @@
import android.graphics.Color;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class Capabilities {
@@ -16,6 +15,8 @@ public class Capabilities {
@Nullable
private String eTag;
+ private boolean directEditingAvailable;
+
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
@@ -49,7 +50,15 @@ public void setTextColor(@ColorInt int textColor) {
this.textColor = textColor;
}
- @NonNull
+
+ public boolean isDirectEditingAvailable() {
+ return directEditingAvailable;
+ }
+
+ public void setDirectEditingAvailable(boolean directEditingAvailable) {
+ this.directEditingAvailable = directEditingAvailable;
+ }
+
@Override
public String toString() {
return "Capabilities{" +
@@ -57,6 +66,7 @@ public String toString() {
", color=" + color +
", textColor=" + textColor +
", eTag='" + eTag + '\'' +
+ ", hasDirectEditing=" + directEditingAvailable +
'}';
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUrl.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUrl.kt
new file mode 100644
index 000000000..bc9e2a14f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUrl.kt
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.shared.model
+
+import com.google.gson.annotations.Expose
+
+data class OcsUrl(
+ @Expose
+ @JvmField
+ var url: String? = null
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingCreator.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingCreator.kt
new file mode 100644
index 000000000..7fb7b5acc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingCreator.kt
@@ -0,0 +1,18 @@
+package it.niedermann.owncloud.notes.shared.model.directediting
+
+import com.google.gson.annotations.Expose
+
+data class DirectEditingCreator(
+ @Expose
+ val id: String,
+ @Expose
+ val editor: String,
+ @Expose
+ val name: String,
+ @Expose
+ val extension: String,
+ @Expose
+ val mimetype: String,
+ @Expose
+ val templates: Boolean,
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingEditor.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingEditor.kt
new file mode 100644
index 000000000..5e5de2e3d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingEditor.kt
@@ -0,0 +1,19 @@
+package it.niedermann.owncloud.notes.shared.model.directediting
+
+import com.google.gson.annotations.Expose
+
+/**
+ * Editor for direct editing data model
+ */
+data class DirectEditingEditor(
+ @Expose
+ val id: String,
+ @Expose
+ val name: String,
+ @Expose
+ val mimetypes: ArrayList,
+ @Expose
+ val optionalMimetypes: ArrayList,
+ @Expose
+ val secure: Boolean,
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingInfo.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingInfo.kt
new file mode 100644
index 000000000..47131dd30
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingInfo.kt
@@ -0,0 +1,10 @@
+package it.niedermann.owncloud.notes.shared.model.directediting
+
+import com.google.gson.annotations.Expose
+
+data class DirectEditingInfo(
+ @Expose
+ val editors: Map,
+ @Expose
+ val creators: Map,
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingRequestBody.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingRequestBody.kt
new file mode 100644
index 000000000..9bfea16bf
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/directediting/DirectEditingRequestBody.kt
@@ -0,0 +1,12 @@
+package it.niedermann.owncloud.notes.shared.model.directediting
+
+import com.google.gson.annotations.Expose
+
+data class DirectEditingRequestBody(
+ @Expose
+ val path: String,
+ @Expose
+ val editorId: String,
+ @Expose
+ val fileId: Long,
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt
new file mode 100644
index 000000000..a27714fbe
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt
@@ -0,0 +1,65 @@
+package it.niedermann.owncloud.notes.shared.util
+
+import android.view.View
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import com.google.android.material.R
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+
+object ExtendedFabUtil {
+ @JvmStatic
+ fun setExtendedFabVisibility(
+ extendedFab: ExtendedFloatingActionButton,
+ visibility: Boolean,
+ ) {
+ if (visibility) {
+ extendedFab.show()
+ } else {
+ if (extendedFab.isExtended) {
+ extendedFab.hide()
+ } else {
+ if (extendedFab.animation == null) {
+ val animation = AnimationUtils.loadAnimation(
+ extendedFab.context,
+ R.anim.abc_shrink_fade_out_from_bottom,
+ )
+ animation.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationStart(animation: Animation) {}
+ override fun onAnimationEnd(animation: Animation) {
+ extendedFab.visibility = View.GONE
+ }
+
+ override fun onAnimationRepeat(animation: Animation) {}
+ })
+ extendedFab.startAnimation(animation)
+ }
+ }
+ }
+ }
+
+ @JvmStatic
+ fun toggleExtendedOnLongClick(extendedFab: ExtendedFloatingActionButton) {
+ extendedFab.setOnLongClickListener { v: View? ->
+ if (extendedFab.isExtended) {
+ extendedFab.shrink()
+ } else {
+ extendedFab.extend()
+ }
+ true
+ }
+ }
+
+ @JvmStatic
+ fun toggleVisibilityOnScroll(
+ extendedFab: ExtendedFloatingActionButton,
+ scrollY: Int,
+ oldScrollY: Int,
+ ) {
+ @Suppress("ConvertTwoComparisonsToRangeCheck")
+ if (oldScrollY > 0 && scrollY > oldScrollY && extendedFab.isShown) {
+ setExtendedFabVisibility(extendedFab, false)
+ } else if (scrollY < oldScrollY && !extendedFab.isShown) {
+ setExtendedFabVisibility(extendedFab, true)
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/rx/DisposableSet.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/rx/DisposableSet.kt
new file mode 100644
index 000000000..12982ed64
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/rx/DisposableSet.kt
@@ -0,0 +1,16 @@
+package it.niedermann.owncloud.notes.shared.util.rx
+
+import io.reactivex.disposables.Disposable
+
+class DisposableSet {
+ private val disposables = mutableSetOf()
+
+ fun add(disposable: Disposable) {
+ disposables.add(disposable)
+ }
+
+ fun dispose() {
+ disposables.forEach { it.dispose() }
+ disposables.clear()
+ }
+}
diff --git a/app/src/main/res/drawable/ic_notes.xml b/app/src/main/res/drawable/ic_notes.xml
new file mode 100644
index 000000000..7ef43fc14
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notes.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_rich_editing.xml b/app/src/main/res/drawable/ic_rich_editing.xml
new file mode 100644
index 000000000..3f13365f4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rich_editing.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_note_direct_edit.xml b/app/src/main/res/layout/fragment_note_direct_edit.xml
new file mode 100644
index 000000000..cecad852a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_note_direct_edit.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_note_edit.xml b/app/src/main/res/layout/fragment_note_edit.xml
index 75e6ab846..79cef9468 100644
--- a/app/src/main/res/layout/fragment_note_edit.xml
+++ b/app/src/main/res/layout/fragment_note_edit.xml
@@ -64,4 +64,19 @@
app:backgroundTint="@color/defaultBrand"
app:srcCompat="@drawable/ic_keyboard_arrow_down_white_24dp"
tools:visibility="visible" />
-
\ No newline at end of file
+
+
+
diff --git a/app/src/main/res/layout/fragment_note_preview.xml b/app/src/main/res/layout/fragment_note_preview.xml
index bd9aabefa..f9028fb56 100644
--- a/app/src/main/res/layout/fragment_note_preview.xml
+++ b/app/src/main/res/layout/fragment_note_preview.xml
@@ -61,4 +61,19 @@
app:backgroundTint="@color/defaultBrand"
app:srcCompat="@drawable/ic_keyboard_arrow_down_white_24dp"
tools:visibility="visible" />
-
\ No newline at end of file
+
+
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index a7557c374..7894ca845 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -3,6 +3,7 @@
- @string/pref_value_mode_edit
- @string/pref_value_mode_preview
+ - @string/pref_value_mode_direct_edit
- @string/pref_value_mode_last
@@ -15,4 +16,4 @@
- @string/pref_value_theme_dark
- @string/pref_value_theme_system_default
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4458476d7..87ffff577 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -41,6 +41,7 @@
Last month
Display mode for notes
+ Notes opening behaviour
Theme
Monospace font
Font size
@@ -122,6 +123,7 @@
lastNoteMode
backgroundSync
edit
+ directEdit
preview
last
small
@@ -213,10 +215,17 @@
Manage accounts
Formatting
-
- - Open in edit mode
- - Open in preview mode
- - Remember my last selection
+
+ Plain edit mode
+ Plain preview
+ Rich edit mode
+ Remember my last selection
+
+
+ - @string/noteMode_plain_edit
+ - @string/noteMode_plain_preview
+ - @string/noteMode_rich_edit
+ - @string/noteMode_remember_last
@@ -357,4 +366,8 @@
Importing notes…
Importing note %1$d of %2$d…
Account imported.
+ Error while loading rich editing
+ Switch to plain editing
+ Back
+ Mozilla/5.0 (Android) %1$s-android/%2$s
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 810f7ae6f..e9a13a5d1 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -35,6 +35,16 @@
android:summary="%s"
android:title="@string/settings_theme_title" />
+
+
-
-