From 746ced255701c91aa34a3456f62900e88621f3ac Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 20:24:58 +0200 Subject: [PATCH 01/23] prototype of keyboard state --- .../owncloud/notes/edit/NoteEditFragment.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 53ffb3b4..f77bc797 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 @@ -2,7 +2,9 @@ package it.niedermann.owncloud.notes.edit; import android.content.Context; import android.content.SharedPreferences; +import android.graphics.Rect; import android.graphics.Typeface; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -18,6 +20,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.ScrollView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -59,6 +62,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } }; private TextWatcher textWatcher; + private boolean keyboardShown = false; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -138,6 +142,10 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { public void onResume() { super.onResume(); binding.editContent.addTextChangedListener(textWatcher); + + if(keyboardShown){ + Toast.makeText(requireContext(),"OPEN",Toast.LENGTH_SHORT).show(); + } } @Override @@ -171,6 +179,15 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { super.onPause(); binding.editContent.removeTextChangedListener(textWatcher); cancelTimers(); + + final View parentView = ((ViewGroup) requireActivity().findViewById(android.R.id.content)).getChildAt(0); + final int defaultKeyboardHeightDP = 100; + final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); + final Rect rect = new Rect(); + int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); + parentView.getWindowVisibleDisplayFrame(rect); + int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); + keyboardShown = heightDiff >= estimatedKeyboardHeight; } private void cancelTimers() { From 21164560671222934d7517a8a92d3bda0e3330b3 Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 21:33:56 +0200 Subject: [PATCH 02/23] key keyboard open on app switching to background --- .../owncloud/notes/edit/NoteEditFragment.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 f77bc797..79ac851a 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 @@ -144,15 +144,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { binding.editContent.addTextChangedListener(textWatcher); if(keyboardShown){ - Toast.makeText(requireContext(),"OPEN",Toast.LENGTH_SHORT).show(); - } - } - - @Override - protected void onNoteLoaded(Note note) { - super.onNoteLoaded(note); - if (TextUtils.isEmpty(note.getContent())) { - binding.editContent.post(() -> { + binding.editContent.postDelayed(() -> { binding.editContent.requestFocus(); final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); @@ -161,7 +153,24 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } else { Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); } - }); + },100); + } + } + + @Override + protected void onNoteLoaded(Note note) { + super.onNoteLoaded(note); + if (TextUtils.isEmpty(note.getContent())) { + binding.editContent.postDelayed(() -> { + binding.editContent.requestFocus(); + + final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); + } else { + Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); + } + },100); } binding.editContent.setMarkdownString(note.getContent()); From 47d5d577fd4a0d2533a8402ca01fffe93c60fd51 Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 21:36:47 +0200 Subject: [PATCH 03/23] refactoring --- .../owncloud/notes/edit/NoteEditFragment.java | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) 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 79ac851a..56800527 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 @@ -144,16 +144,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { binding.editContent.addTextChangedListener(textWatcher); if(keyboardShown){ - binding.editContent.postDelayed(() -> { - binding.editContent.requestFocus(); - - final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); - } else { - Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); - } - },100); + openSoftKeyboard(); } } @@ -161,16 +152,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { protected void onNoteLoaded(Note note) { super.onNoteLoaded(note); if (TextUtils.isEmpty(note.getContent())) { - binding.editContent.postDelayed(() -> { - binding.editContent.requestFocus(); - - final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); - } else { - Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); - } - },100); + openSoftKeyboard(); } binding.editContent.setMarkdownString(note.getContent()); @@ -183,6 +165,20 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } } + private void openSoftKeyboard(){ + binding.editContent.postDelayed(() -> { + binding.editContent.requestFocus(); + + final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); + } else { + Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); + } + //Without a small delay the keyboard does not show reliably + },100); + } + @Override public void onPause() { super.onPause(); From d68f2dfef20688874d6b836fdee9c602816adbab Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 21:45:32 +0200 Subject: [PATCH 04/23] refactoring --- .../owncloud/notes/edit/NoteEditFragment.java | 9 ++------ .../notes/shared/util/DisplayUtils.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) 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 56800527..cac5fdff 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 @@ -32,6 +32,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.util.DisplayUtils; import static androidx.core.view.ViewCompat.isAttachedToWindow; import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; @@ -186,13 +187,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { cancelTimers(); final View parentView = ((ViewGroup) requireActivity().findViewById(android.R.id.content)).getChildAt(0); - final int defaultKeyboardHeightDP = 100; - final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); - final Rect rect = new Rect(); - int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); - parentView.getWindowVisibleDisplayFrame(rect); - int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); - keyboardShown = heightDiff >= estimatedKeyboardHeight; + keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView); } private void cancelTimers() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index b0adc011..80ac7de4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -3,10 +3,14 @@ package it.niedermann.owncloud.notes.shared.util; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; import android.text.Spannable; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.MetricAffectingSpan; +import android.util.TypedValue; +import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; @@ -52,4 +56,21 @@ public class DisplayUtils { } return new NavigationItem.CategoryNavigationItem("category:" + counter.getCategory(), counter.getCategory(), counter.getTotalNotes(), icon, counter.getAccountId(), counter.getCategory()); } + + /** + * Android does not provide a way to get keyboard visibility prior to API 30 so we use a workaround + * @param parentView View + * @return keyboardVisibility Boolean + */ + public static boolean isSoftKeyboardVisible(View parentView){ + //Arbitrary keyboard height + final int defaultKeyboardHeightDP = 100; + final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); + final Rect rect = new Rect(); + + int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); + parentView.getWindowVisibleDisplayFrame(rect); + int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); + return heightDiff >= estimatedKeyboardHeight; + } } From 2405959324ce59562644fdac8f8dc0be1431bde9 Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 21:52:07 +0200 Subject: [PATCH 05/23] remove unused imports --- .../it/niedermann/owncloud/notes/edit/NoteEditFragment.java | 3 --- 1 file changed, 3 deletions(-) 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 cac5fdff..630d878a 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 @@ -2,9 +2,7 @@ package it.niedermann.owncloud.notes.edit; import android.content.Context; import android.content.SharedPreferences; -import android.graphics.Rect; import android.graphics.Typeface; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -20,7 +18,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.ScrollView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; From 988ce872ccbfb5f96825d90eea811e5e1a713cfc Mon Sep 17 00:00:00 2001 From: drhaal Date: Tue, 8 Jun 2021 21:58:08 +0200 Subject: [PATCH 06/23] reformat code --- .../it/niedermann/owncloud/notes/edit/NoteEditFragment.java | 6 +++--- .../niedermann/owncloud/notes/shared/util/DisplayUtils.java | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) 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 630d878a..6ba09432 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 @@ -141,7 +141,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { super.onResume(); binding.editContent.addTextChangedListener(textWatcher); - if(keyboardShown){ + if (keyboardShown) { openSoftKeyboard(); } } @@ -163,7 +163,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } } - private void openSoftKeyboard(){ + private void openSoftKeyboard() { binding.editContent.postDelayed(() -> { binding.editContent.requestFocus(); @@ -174,7 +174,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); } //Without a small delay the keyboard does not show reliably - },100); + }, 100); } @Override diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index 80ac7de4..297c843c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -58,11 +58,12 @@ public class DisplayUtils { } /** - * Android does not provide a way to get keyboard visibility prior to API 30 so we use a workaround + * Android does not provide a way to get keyboard visibility prior to API 30 so we use a workaround + * * @param parentView View * @return keyboardVisibility Boolean */ - public static boolean isSoftKeyboardVisible(View parentView){ + public static boolean isSoftKeyboardVisible(View parentView) { //Arbitrary keyboard height final int defaultKeyboardHeightDP = 100; final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); From 7a59c808a34b7fe472cc681065154bba2098ae2d Mon Sep 17 00:00:00 2001 From: drhaal Date: Thu, 10 Jun 2021 20:46:56 +0200 Subject: [PATCH 07/23] check for nulls + use proper keyboard detection on API 30+ --- .../owncloud/notes/edit/NoteEditFragment.java | 9 +++++++-- .../owncloud/notes/shared/util/DisplayUtils.java | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) 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 6ba09432..11e1ec4f 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 @@ -16,6 +16,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.inputmethod.InputMethodManager; import android.widget.ScrollView; @@ -183,8 +184,12 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { binding.editContent.removeTextChangedListener(textWatcher); cancelTimers(); - final View parentView = ((ViewGroup) requireActivity().findViewById(android.R.id.content)).getChildAt(0); - keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView); + final ViewGroup parentView = requireActivity().findViewById(android.R.id.content); + if(parentView != null && parentView.getChildCount() > 0){ + keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView.getChildAt(0)); + }else { + keyboardShown = false; + } } private void cancelTimers() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index 297c843c..ffea98b1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -11,11 +11,14 @@ import android.text.TextUtils; import android.text.style.MetricAffectingSpan; import android.util.TypedValue; import android.view.View; +import android.view.WindowInsets; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.Collection; import java.util.List; @@ -58,12 +61,21 @@ public class DisplayUtils { } /** - * Android does not provide a way to get keyboard visibility prior to API 30 so we use a workaround + * Detect if the soft keyboard is open. + * On API prior to 30 we fall back to workaround which might be less reliable * * @param parentView View * @return keyboardVisibility Boolean */ - public static boolean isSoftKeyboardVisible(View parentView) { + public static boolean isSoftKeyboardVisible(@NonNull View parentView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView); + if(insets != null){ + return insets.isVisible(WindowInsets.Type.ime()); + } + } + //Fall Back to workaround + //Arbitrary keyboard height final int defaultKeyboardHeightDP = 100; final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); From db3513961c4da6c491d8ff2a58deb8b7f640bafb Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 11:38:43 +0200 Subject: [PATCH 08/23] Apply ToggleTaskListSpan only for areas without ClickableSpan Signed-off-by: Stefan Niedermann --- .../notes/edit/NotePreviewFragment.java | 1 + .../plugins/LinkClickInterceptorPlugin.java | 28 +-- .../plugins/ToggleableTaskListPlugin.java | 184 +++++++++++------- .../markwon/span/InterceptedURLSpan.java | 3 +- .../markwon/span/ToggleTaskListSpan.java | 37 +--- 5 files changed, 129 insertions(+), 124 deletions(-) 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 f15d0e59..dfcf8db2 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 @@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.edit; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import android.text.Layout; diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java index e815b54b..81c48aa2 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java @@ -1,22 +1,19 @@ package it.niedermann.android.markdown.markwon.plugins; -import android.text.Spannable; -import android.text.style.URLSpan; -import android.widget.TextView; - import androidx.annotation.NonNull; +import org.commonmark.node.Link; + import java.util.Collection; import java.util.LinkedList; import java.util.function.Function; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonPlugin; +import io.noties.markwon.MarkwonSpansFactory; +import io.noties.markwon.core.CoreProps; import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan; -import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; -import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable; - public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin { @NonNull @@ -27,20 +24,9 @@ public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin { } @Override - public void afterSetText(@NonNull TextView textView) { - super.afterSetText(textView); - if (onLinkClickCallbacks.size() > 0) { - final Spannable spannable = getContentAsSpannable(textView); - final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); - - for (URLSpan originalSpan : spans) { - final InterceptedURLSpan interceptedSpan = new InterceptedURLSpan(onLinkClickCallbacks, originalSpan.getURL()); - final int start = spannable.getSpanStart(originalSpan); - final int end = spannable.getSpanEnd(originalSpan); - spannable.removeSpan(originalSpan); - spannable.setSpan(interceptedSpan, start, end, SPAN_EXCLUSIVE_EXCLUSIVE); - } - } + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + super.configureSpansFactory(builder); + builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props))); } public void registerOnLinkClickCallback(@NonNull Function callback) { diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index 740e138b..d172f333 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -1,19 +1,20 @@ package it.niedermann.android.markdown.markwon.plugins; -import android.util.Log; +import android.text.Spannable; +import android.text.style.ClickableSpan; +import android.util.Range; import androidx.annotation.NonNull; -import org.commonmark.node.AbstractVisitor; -import org.commonmark.node.Block; -import org.commonmark.node.HardLineBreak; import org.commonmark.node.Node; -import org.commonmark.node.Paragraph; -import org.commonmark.node.SoftLineBreak; -import org.commonmark.node.Text; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonVisitor; @@ -56,32 +57,6 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { .get(TaskListItem.class); final Object spans = spanFactory == null ? null : spanFactory.getSpans(visitor.configuration(), visitor.renderProps()); - - if (spans != null) { - final TaskListSpan taskListSpan; - if (spans instanceof TaskListSpan[]) { - if (((TaskListSpan[]) spans).length > 0) { - taskListSpan = ((TaskListSpan[]) spans)[0]; - } else { - taskListSpan = null; - } - } else if (spans instanceof TaskListSpan) { - taskListSpan = (TaskListSpan) spans; - } else { - taskListSpan = null; - } - - final int content = TaskListContextVisitor.contentLength(node); - if (content > 0 && taskListSpan != null) { - // maybe additionally identify this task list (for persistence) - visitor.builder().setSpan( - new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, visitor.builder().subSequence(length, length + content).toString()), - length, - length + content - ); - } - } - SpannableBuilder.setSpans( visitor.builder(), spans, @@ -95,48 +70,111 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { }); } - static class TaskListContextVisitor extends AbstractVisitor { - private int contentLength = 0; + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + super.afterRender(node, visitor); + final Spannable spanned = visitor.builder().spannableStringBuilder(); + final List spans = visitor.builder().getSpans(0, visitor.builder().length()); + final List taskListSpans = spans.stream() + .filter(span -> span.what instanceof TaskListSpan) + .map(span -> ((TaskListSpan) span.what)) + .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) + .collect(Collectors.toList()); - static int contentLength(Node node) { - final TaskListContextVisitor visitor = new TaskListContextVisitor(); - visitor.visitChildren(node); - return visitor.contentLength; - } - - @Override - public void visit(Text text) { - super.visit(text); - contentLength += text.getLiteral().length(); - } - - // NB! if count both soft and hard breaks as having length of 1 - @Override - public void visit(SoftLineBreak softLineBreak) { - super.visit(softLineBreak); - contentLength += 1; - } - - // NB! if count both soft and hard breaks as having length of 1 - @Override - public void visit(HardLineBreak hardLineBreak) { - super.visit(hardLineBreak); - contentLength += 1; - } - - @Override - protected void visitChildren(Node parent) { - Node node = parent.getFirstChild(); - while (node != null) { - // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no - // node after visiting it. So get the next node before visiting. - Node next = node.getNext(); - if (node instanceof Block && !(node instanceof Paragraph)) { - break; - } - node.accept(this); - node = next; + for (int position = 0; position < taskListSpans.size(); position++) { + final TaskListSpan taskListSpan = taskListSpans.get(position); + final int start = spanned.getSpanStart(taskListSpan); +// final int contentLength = TaskListContextVisitor.contentLength(node); +// final int end = start + contentLength; + final int end = spanned.getSpanEnd(taskListSpan); + final List> freeRanges = findFreeRanges(spanned, start, end); + for (Range freeRange : freeRanges) { + visitor.builder().setSpan( + new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, position), + freeRange.getLower(), freeRange.getUpper()); } } } + + /** + * @return a list of ranges in the given {@param spanned} from {@param start} to {@param end} which is not taken for a {@link ClickableSpan}. + */ + @NonNull + private static List> findFreeRanges(@NonNull Spannable spanned, int start, int end) { + final List> freeRanges; + final List clickableSpans = getClickableSpans(spanned, start, end); + if (clickableSpans.size() > 0) { + freeRanges = new ArrayList<>(clickableSpans.size()); + int from = start; + for (ClickableSpan clickableSpan : clickableSpans) { + final int clickableStart = spanned.getSpanStart(clickableSpan); + final int clickableEnd = spanned.getSpanEnd(clickableSpan); + if (from != clickableStart) { + freeRanges.add(new Range<>(from, clickableStart)); + } + from = clickableEnd; + } + if (clickableSpans.size() > 0) { + final int lastUpperBlocker = spanned.getSpanEnd(clickableSpans.get(clickableSpans.size() - 1)); + if (lastUpperBlocker < end) { + freeRanges.add(new Range<>(lastUpperBlocker, end)); + } + } + } else { + freeRanges = Collections.singletonList(new Range<>(start, end)); + } + return freeRanges; + } + + @NonNull + private static List getClickableSpans(@NonNull Spannable spanned, int start, int end) { + return Arrays.stream(spanned.getSpans(start, end, ClickableSpan.class)) + .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) + .collect(Collectors.toList()); + } + +// static class TaskListContextVisitor extends AbstractVisitor { +// private int contentLength = 0; +// +// static int contentLength(Node node) { +// final TaskListContextVisitor visitor = new TaskListContextVisitor(); +// visitor.visitChildren(node); +// return visitor.contentLength; +// } +// +// @Override +// public void visit(Text text) { +// super.visit(text); +// contentLength += text.getLiteral().length(); +// } +// +// // NB! if count both soft and hard breaks as having length of 1 +// @Override +// public void visit(SoftLineBreak softLineBreak) { +// super.visit(softLineBreak); +// contentLength += 1; +// } +// +// // NB! if count both soft and hard breaks as having length of 1 +// @Override +// public void visit(HardLineBreak hardLineBreak) { +// super.visit(hardLineBreak); +// contentLength += 1; +// } +// +// @Override +// protected void visitChildren(Node parent) { +// Node node = parent.getFirstChild(); +// while (node != null) { +// // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no +// // node after visiting it. So get the next node before visiting. +// Node next = node.getNext(); +// if (node instanceof Block && !(node instanceof Paragraph)) { +// break; +// } +// node.accept(this); +// node = next; +// } +// } +// } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java index 35c456e0..f370c006 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java @@ -1,8 +1,10 @@ package it.niedermann.android.markdown.markwon.span; +import android.text.Spanned; import android.text.style.URLSpan; import android.util.Log; import android.view.View; +import android.widget.TextView; import androidx.annotation.NonNull; @@ -22,7 +24,6 @@ public class InterceptedURLSpan extends URLSpan { super(url); this.onLinkClickCallbacks = onLinkClickCallbacks; } - @Override public void onClick(View widget) { if (onLinkClickCallbacks.size() > 0) { diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java index a38ac65e..2b432602 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java @@ -1,15 +1,12 @@ package it.niedermann.android.markdown.markwon.span; -import android.text.Spanned; import android.text.TextPaint; import android.text.style.ClickableSpan; import android.util.Log; import android.view.View; -import android.widget.TextView; import androidx.annotation.NonNull; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; @@ -19,42 +16,24 @@ public class ToggleTaskListSpan extends ClickableSpan { private static final String TAG = ToggleTaskListSpan.class.getSimpleName(); - final AtomicBoolean enabled; - final BiConsumer toggleListener; - final TaskListSpan span; - final String content; + private final AtomicBoolean enabled; + private final BiConsumer toggleListener; + private final TaskListSpan span; + private final int position; - public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer toggleListener, @NonNull TaskListSpan span, String content) { + public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer toggleListener, @NonNull TaskListSpan span, int position) { this.enabled = enabled; this.toggleListener = toggleListener; this.span = span; - this.content = content; + this.position = position; } @Override public void onClick(@NonNull View widget) { - if(enabled.get()) { + if (enabled.get()) { span.setDone(!span.isDone()); widget.invalidate(); - - // it must be a TextView - final TextView textView = (TextView) widget; - // it must be spanned - // TODO what if textView is not a spanned? - final Spanned spanned = (Spanned) textView.getText(); - - final ClickableSpan[] toggles = spanned.getSpans(0, spanned.length(), getClass()); - Arrays.sort(toggles, (o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)); - - int currentTogglePosition = -1; - for (int i = 0; i < toggles.length; i++) { - if (spanned.getSpanStart(toggles[i]) == spanned.getSpanStart(this) && spanned.getSpanEnd(toggles[i]) == spanned.getSpanEnd(this)) { - currentTogglePosition = i; - break; - } - } - - toggleListener.accept(currentTogglePosition, span.isDone()); + toggleListener.accept(position, span.isDone()); } else { Log.w(TAG, "Prevented toggling checkbox because the view is disabled"); } From f2c3883dfec8c9424238262673cbeb2878319ba7 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 11:51:48 +0200 Subject: [PATCH 09/23] Apply ToggleTaskListSpan only for areas without ClickableSpan Signed-off-by: Stefan Niedermann --- .../plugins/ToggleableTaskListPlugin.java | 163 ++++++++++++------ 1 file changed, 108 insertions(+), 55 deletions(-) diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index d172f333..69a05878 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -6,7 +6,13 @@ import android.util.Range; import androidx.annotation.NonNull; +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.Block; +import org.commonmark.node.HardLineBreak; import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.Text; import java.util.ArrayList; import java.util.Arrays; @@ -46,6 +52,9 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { this.enabled.set(enabled); } + /** + * Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step. + */ @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(TaskListItem.class, (visitor, node) -> { @@ -57,6 +66,30 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { .get(TaskListItem.class); final Object spans = spanFactory == null ? null : spanFactory.getSpans(visitor.configuration(), visitor.renderProps()); + if (spans != null) { + final TaskListSpan taskListSpan; + if (spans instanceof TaskListSpan[]) { + if (((TaskListSpan[]) spans).length > 0) { + taskListSpan = ((TaskListSpan[]) spans)[0]; + } else { + taskListSpan = null; + } + } else if (spans instanceof TaskListSpan) { + taskListSpan = (TaskListSpan) spans; + } else { + taskListSpan = null; + } + + final int content = TaskListContextVisitor.contentLength(node); + if (content > 0 && taskListSpan != null) { + // maybe additionally identify this task list (for persistence) + visitor.builder().setSpan( + new ToggleMarkerSpan(taskListSpan), + length, + length + content + ); + } + } SpannableBuilder.setSpans( visitor.builder(), spans, @@ -70,29 +103,31 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { }); } + /** + * Converts all {@link ToggleMarkerSpan}s to actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. + */ @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { super.afterRender(node, visitor); final Spannable spanned = visitor.builder().spannableStringBuilder(); - final List spans = visitor.builder().getSpans(0, visitor.builder().length()); - final List taskListSpans = spans.stream() - .filter(span -> span.what instanceof TaskListSpan) - .map(span -> ((TaskListSpan) span.what)) + final List markerSpans = visitor.builder().getSpans(0, visitor.builder().length()) + .stream() + .filter(span -> span.what instanceof ToggleMarkerSpan) + .map(span -> ((ToggleMarkerSpan) span.what)) .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) .collect(Collectors.toList()); - for (int position = 0; position < taskListSpans.size(); position++) { - final TaskListSpan taskListSpan = taskListSpans.get(position); - final int start = spanned.getSpanStart(taskListSpan); -// final int contentLength = TaskListContextVisitor.contentLength(node); -// final int end = start + contentLength; - final int end = spanned.getSpanEnd(taskListSpan); + for (int position = 0; position < markerSpans.size(); position++) { + final ToggleMarkerSpan markerSpan = markerSpans.get(position); + final int start = spanned.getSpanStart(markerSpan); + final int end = spanned.getSpanEnd(markerSpan); final List> freeRanges = findFreeRanges(spanned, start, end); for (Range freeRange : freeRanges) { visitor.builder().setSpan( - new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, position), + new ToggleTaskListSpan(enabled, toggleListener, markerSpan.getTaskListSpan(), position), freeRange.getLower(), freeRange.getUpper()); } + spanned.removeSpan(markerSpan); } } @@ -133,48 +168,66 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { .collect(Collectors.toList()); } -// static class TaskListContextVisitor extends AbstractVisitor { -// private int contentLength = 0; -// -// static int contentLength(Node node) { -// final TaskListContextVisitor visitor = new TaskListContextVisitor(); -// visitor.visitChildren(node); -// return visitor.contentLength; -// } -// -// @Override -// public void visit(Text text) { -// super.visit(text); -// contentLength += text.getLiteral().length(); -// } -// -// // NB! if count both soft and hard breaks as having length of 1 -// @Override -// public void visit(SoftLineBreak softLineBreak) { -// super.visit(softLineBreak); -// contentLength += 1; -// } -// -// // NB! if count both soft and hard breaks as having length of 1 -// @Override -// public void visit(HardLineBreak hardLineBreak) { -// super.visit(hardLineBreak); -// contentLength += 1; -// } -// -// @Override -// protected void visitChildren(Node parent) { -// Node node = parent.getFirstChild(); -// while (node != null) { -// // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no -// // node after visiting it. So get the next node before visiting. -// Node next = node.getNext(); -// if (node instanceof Block && !(node instanceof Paragraph)) { -// break; -// } -// node.accept(this); -// node = next; -// } -// } -// } + private static final class TaskListContextVisitor extends AbstractVisitor { + private int contentLength = 0; + + static int contentLength(Node node) { + final TaskListContextVisitor visitor = new TaskListContextVisitor(); + visitor.visitChildren(node); + return visitor.contentLength; + } + + @Override + public void visit(Text text) { + super.visit(text); + contentLength += text.getLiteral().length(); + } + + // NB! if count both soft and hard breaks as having length of 1 + @Override + public void visit(SoftLineBreak softLineBreak) { + super.visit(softLineBreak); + contentLength += 1; + } + + // NB! if count both soft and hard breaks as having length of 1 + @Override + public void visit(HardLineBreak hardLineBreak) { + super.visit(hardLineBreak); + contentLength += 1; + } + + @Override + protected void visitChildren(Node parent) { + Node node = parent.getFirstChild(); + while (node != null) { + // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no + // node after visiting it. So get the next node before visiting. + Node next = node.getNext(); + if (node instanceof Block && !(node instanceof Paragraph)) { + break; + } + node.accept(this); + node = next; + } + } + } + + /** + * Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s. + */ + private static final class ToggleMarkerSpan { + + @NonNull + private final TaskListSpan taskListSpan; + + private ToggleMarkerSpan(@NonNull TaskListSpan taskListSpan) { + this.taskListSpan = taskListSpan; + } + + @NonNull + private TaskListSpan getTaskListSpan() { + return taskListSpan; + } + } } From fc598b84c94765e603c364abdfa568454443aa2d Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 11:51:48 +0200 Subject: [PATCH 10/23] Remove unused import Signed-off-by: Stefan Niedermann --- .../notes/edit/NotePreviewFragment.java | 1 - .../plugins/ToggleableTaskListPlugin.java | 163 ++++++++++++------ 2 files changed, 108 insertions(+), 56 deletions(-) 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 dfcf8db2..f15d0e59 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 @@ -2,7 +2,6 @@ package it.niedermann.owncloud.notes.edit; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import android.text.Layout; diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index d172f333..69a05878 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -6,7 +6,13 @@ import android.util.Range; import androidx.annotation.NonNull; +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.Block; +import org.commonmark.node.HardLineBreak; import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.Text; import java.util.ArrayList; import java.util.Arrays; @@ -46,6 +52,9 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { this.enabled.set(enabled); } + /** + * Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step. + */ @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(TaskListItem.class, (visitor, node) -> { @@ -57,6 +66,30 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { .get(TaskListItem.class); final Object spans = spanFactory == null ? null : spanFactory.getSpans(visitor.configuration(), visitor.renderProps()); + if (spans != null) { + final TaskListSpan taskListSpan; + if (spans instanceof TaskListSpan[]) { + if (((TaskListSpan[]) spans).length > 0) { + taskListSpan = ((TaskListSpan[]) spans)[0]; + } else { + taskListSpan = null; + } + } else if (spans instanceof TaskListSpan) { + taskListSpan = (TaskListSpan) spans; + } else { + taskListSpan = null; + } + + final int content = TaskListContextVisitor.contentLength(node); + if (content > 0 && taskListSpan != null) { + // maybe additionally identify this task list (for persistence) + visitor.builder().setSpan( + new ToggleMarkerSpan(taskListSpan), + length, + length + content + ); + } + } SpannableBuilder.setSpans( visitor.builder(), spans, @@ -70,29 +103,31 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { }); } + /** + * Converts all {@link ToggleMarkerSpan}s to actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. + */ @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { super.afterRender(node, visitor); final Spannable spanned = visitor.builder().spannableStringBuilder(); - final List spans = visitor.builder().getSpans(0, visitor.builder().length()); - final List taskListSpans = spans.stream() - .filter(span -> span.what instanceof TaskListSpan) - .map(span -> ((TaskListSpan) span.what)) + final List markerSpans = visitor.builder().getSpans(0, visitor.builder().length()) + .stream() + .filter(span -> span.what instanceof ToggleMarkerSpan) + .map(span -> ((ToggleMarkerSpan) span.what)) .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) .collect(Collectors.toList()); - for (int position = 0; position < taskListSpans.size(); position++) { - final TaskListSpan taskListSpan = taskListSpans.get(position); - final int start = spanned.getSpanStart(taskListSpan); -// final int contentLength = TaskListContextVisitor.contentLength(node); -// final int end = start + contentLength; - final int end = spanned.getSpanEnd(taskListSpan); + for (int position = 0; position < markerSpans.size(); position++) { + final ToggleMarkerSpan markerSpan = markerSpans.get(position); + final int start = spanned.getSpanStart(markerSpan); + final int end = spanned.getSpanEnd(markerSpan); final List> freeRanges = findFreeRanges(spanned, start, end); for (Range freeRange : freeRanges) { visitor.builder().setSpan( - new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, position), + new ToggleTaskListSpan(enabled, toggleListener, markerSpan.getTaskListSpan(), position), freeRange.getLower(), freeRange.getUpper()); } + spanned.removeSpan(markerSpan); } } @@ -133,48 +168,66 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { .collect(Collectors.toList()); } -// static class TaskListContextVisitor extends AbstractVisitor { -// private int contentLength = 0; -// -// static int contentLength(Node node) { -// final TaskListContextVisitor visitor = new TaskListContextVisitor(); -// visitor.visitChildren(node); -// return visitor.contentLength; -// } -// -// @Override -// public void visit(Text text) { -// super.visit(text); -// contentLength += text.getLiteral().length(); -// } -// -// // NB! if count both soft and hard breaks as having length of 1 -// @Override -// public void visit(SoftLineBreak softLineBreak) { -// super.visit(softLineBreak); -// contentLength += 1; -// } -// -// // NB! if count both soft and hard breaks as having length of 1 -// @Override -// public void visit(HardLineBreak hardLineBreak) { -// super.visit(hardLineBreak); -// contentLength += 1; -// } -// -// @Override -// protected void visitChildren(Node parent) { -// Node node = parent.getFirstChild(); -// while (node != null) { -// // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no -// // node after visiting it. So get the next node before visiting. -// Node next = node.getNext(); -// if (node instanceof Block && !(node instanceof Paragraph)) { -// break; -// } -// node.accept(this); -// node = next; -// } -// } -// } + private static final class TaskListContextVisitor extends AbstractVisitor { + private int contentLength = 0; + + static int contentLength(Node node) { + final TaskListContextVisitor visitor = new TaskListContextVisitor(); + visitor.visitChildren(node); + return visitor.contentLength; + } + + @Override + public void visit(Text text) { + super.visit(text); + contentLength += text.getLiteral().length(); + } + + // NB! if count both soft and hard breaks as having length of 1 + @Override + public void visit(SoftLineBreak softLineBreak) { + super.visit(softLineBreak); + contentLength += 1; + } + + // NB! if count both soft and hard breaks as having length of 1 + @Override + public void visit(HardLineBreak hardLineBreak) { + super.visit(hardLineBreak); + contentLength += 1; + } + + @Override + protected void visitChildren(Node parent) { + Node node = parent.getFirstChild(); + while (node != null) { + // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no + // node after visiting it. So get the next node before visiting. + Node next = node.getNext(); + if (node instanceof Block && !(node instanceof Paragraph)) { + break; + } + node.accept(this); + node = next; + } + } + } + + /** + * Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s. + */ + private static final class ToggleMarkerSpan { + + @NonNull + private final TaskListSpan taskListSpan; + + private ToggleMarkerSpan(@NonNull TaskListSpan taskListSpan) { + this.taskListSpan = taskListSpan; + } + + @NonNull + private TaskListSpan getTaskListSpan() { + return taskListSpan; + } + } } From f57603d4726b09573c29cc825e6d963674ef7a44 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 15:20:29 +0200 Subject: [PATCH 11/23] Refactor links in checkboxes and add unit tests Signed-off-by: Stefan Niedermann --- markdown/build.gradle | 3 + .../plugins/ToggleableTaskListPlugin.java | 86 ++++---- .../plugins/ToggleableTaskListPluginTest.java | 200 ++++++++++++++++++ 3 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java diff --git a/markdown/build.gradle b/markdown/build.gradle index 91d8d437..a9107750 100644 --- a/markdown/build.gradle +++ b/markdown/build.gradle @@ -57,6 +57,9 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'androidx.arch.core:core-testing:2.1.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.11.0' testImplementation 'org.robolectric:robolectric:4.5.1' } \ No newline at end of file diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index b43c5711..77411392 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -3,8 +3,10 @@ package it.niedermann.android.markdown.markwon.plugins; import android.text.Spannable; import android.text.style.ClickableSpan; import android.util.Range; +import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import org.commonmark.node.AbstractVisitor; import org.commonmark.node.Block; @@ -15,7 +17,6 @@ import org.commonmark.node.SoftLineBreak; import org.commonmark.node.Text; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -29,6 +30,7 @@ import io.noties.markwon.SpannableBuilder; import io.noties.markwon.ext.tasklist.TaskListItem; import io.noties.markwon.ext.tasklist.TaskListProps; import io.noties.markwon.ext.tasklist.TaskListSpan; +import it.niedermann.android.markdown.MarkdownUtil; import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan; /** @@ -103,81 +105,92 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { }); } + + /** + * Adds for each {@link ToggleMarkerSpan} and actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. + */ @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { super.afterRender(node, visitor); - final Spannable spanned = visitor.builder().spannableStringBuilder(); - final List markerSpans = getSortedToggleMarkerSpans(spanned, visitor.builder()); + final List markerSpans = getSortedToggleMarkerSpans(visitor.builder()); - replaceMarkerSpans(markerSpans, spanned, visitor.builder()); - } - - /** - * Converts all {@link ToggleMarkerSpan}s to actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. - */ - private void replaceMarkerSpans(@NonNull List markerSpans, @NonNull Spannable spanned, @NonNull SpannableBuilder builder) { for (int position = 0; position < markerSpans.size(); position++) { - final ToggleMarkerSpan markerSpan = markerSpans.get(position); - final int start = spanned.getSpanStart(markerSpan); - final int end = spanned.getSpanEnd(markerSpan); - final List> freeRanges = findFreeRanges(spanned, start, end); + final SpannableBuilder.Span markerSpan = markerSpans.get(position); + final int start = markerSpan.start; + final int end = markerSpan.end; + final List> freeRanges = findFreeRanges(visitor.builder(), start, end); for (Range freeRange : freeRanges) { - builder.setSpan( - new ToggleTaskListSpan(enabled, toggleListener, markerSpan.getTaskListSpan(), position), + visitor.builder().setSpan( + new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position), freeRange.getLower(), freeRange.getUpper()); } - spanned.removeSpan(markerSpan); } } /** - * @return a {@link List} of {@link ToggleMarkerSpan}s, sorted ascending by the span start. + * Removes {@link ToggleMarkerSpan}s from {@param textView}. */ - @NonNull - private static List getSortedToggleMarkerSpans(@NonNull Spannable spanned, @NonNull SpannableBuilder builder) { - return builder.getSpans(0, builder.length()) - .stream() - .filter(span -> span.what instanceof ToggleMarkerSpan) - .map(span -> ((ToggleMarkerSpan) span.what)) - .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) - .collect(Collectors.toList()); + @Override + public void afterSetText(@NonNull TextView textView) { + super.afterSetText(textView); + final Spannable spannable = MarkdownUtil.getContentAsSpannable(textView); + for (ToggleMarkerSpan span : spannable.getSpans(0, spannable.length(), ToggleMarkerSpan.class)) { + spannable.removeSpan(span); + } + textView.setText(spannable); } /** * @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is not taken for a {@link ClickableSpan}. */ @NonNull - private static List> findFreeRanges(@NonNull Spannable spanned, int start, int end) { + private static List> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) { final List> freeRanges; - final List clickableSpans = getClickableSpans(spanned, start, end); + final List clickableSpans = getClickableSpans(builder, start, end); if (clickableSpans.size() > 0) { freeRanges = new ArrayList<>(clickableSpans.size()); int from = start; - for (ClickableSpan clickableSpan : clickableSpans) { - final int clickableStart = spanned.getSpanStart(clickableSpan); - final int clickableEnd = spanned.getSpanEnd(clickableSpan); + for (SpannableBuilder.Span clickableSpan : clickableSpans) { + final int clickableStart = clickableSpan.start; + final int clickableEnd = clickableSpan.end; if (from != clickableStart) { freeRanges.add(new Range<>(from, clickableStart)); } from = clickableEnd; } if (clickableSpans.size() > 0) { - final int lastUpperBlocker = spanned.getSpanEnd(clickableSpans.get(clickableSpans.size() - 1)); + final int lastUpperBlocker = clickableSpans.get(clickableSpans.size() - 1).end; if (lastUpperBlocker < end) { freeRanges.add(new Range<>(lastUpperBlocker, end)); } } + } else if (start == end) { + freeRanges = Collections.emptyList(); } else { freeRanges = Collections.singletonList(new Range<>(start, end)); } return freeRanges; } + /** + * @return a {@link List} of {@link ToggleMarkerSpan}s, sorted ascending by the span start. + */ @NonNull - private static List getClickableSpans(@NonNull Spannable spanned, int start, int end) { - return Arrays.stream(spanned.getSpans(start, end, ClickableSpan.class)) - .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2)) + private static List getSortedToggleMarkerSpans(@NonNull SpannableBuilder builder) { + return builder.getSpans(0, builder.length()) + .stream() + .filter(span -> span.what instanceof ToggleMarkerSpan) + .sorted((o1, o2) -> o1.start - o2.start) + .collect(Collectors.toList()); + } + + @NonNull + private static List getClickableSpans(@NonNull SpannableBuilder builder, int start, int end) { + return builder.getSpans(start, end) + .stream() + .filter(span -> span.what instanceof ClickableSpan) + .sorted((o1, o2) -> o1.start - o2.start) .collect(Collectors.toList()); } @@ -229,7 +242,8 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { /** * Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s. */ - private static final class ToggleMarkerSpan { + @VisibleForTesting + static final class ToggleMarkerSpan { @NonNull private final TaskListSpan taskListSpan; diff --git a/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java new file mode 100644 index 00000000..823e1e13 --- /dev/null +++ b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java @@ -0,0 +1,200 @@ +package it.niedermann.android.markdown.markwon.plugins; + +import android.text.Editable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.util.Range; +import android.widget.TextView; + +import androidx.test.core.app.ApplicationProvider; + +import junit.framework.TestCase; + +import org.commonmark.node.Node; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.ext.tasklist.TaskListSpan; +import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan; +import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class ToggleableTaskListPluginTest extends TestCase { + + @Test + public void testAfterRender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { + final Node node = mock(Node.class); + final MarkwonVisitor visitor = mock(MarkwonVisitor.class); + + final Constructor markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class); + markerSpanConstructor.setAccessible(true); + + final SpannableBuilder builder = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet"); + builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6); + builder.setSpan(new URLSpan(""), 6, 11); + builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19); + builder.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22); + builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27); + + when(visitor.builder()).thenReturn(builder); + + final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> { + // Do nothing... + }); + plugin.afterRender(node, visitor); + + // We ignore marker spans in this test. They will be removed in another step + final List spans = builder.getSpans(0, builder.length()) + .stream() + .filter(span -> span.what.getClass() != ToggleableTaskListPlugin.ToggleMarkerSpan.class) + .sorted((o1, o2) -> o1.start - o2.start) + .collect(Collectors.toList()); + + assertEquals(5, spans.size()); + assertEquals(ToggleTaskListSpan.class, spans.get(0).what.getClass()); + assertEquals(0, spans.get(0).start); + assertEquals(6, spans.get(0).end); + assertEquals(URLSpan.class, spans.get(1).what.getClass()); + assertEquals(6, spans.get(1).start); + assertEquals(11, spans.get(1).end); + assertEquals(ToggleTaskListSpan.class, spans.get(2).what.getClass()); + assertEquals(11, spans.get(2).start); + assertEquals(19, spans.get(2).end); + assertEquals(InterceptedURLSpan.class, spans.get(3).what.getClass()); + assertEquals(19, spans.get(3).start); + assertEquals(22, spans.get(3).end); + assertEquals(ToggleTaskListSpan.class, spans.get(4).what.getClass()); + assertEquals(22, spans.get(4).start); + assertEquals(27, spans.get(4).end); + } + + @Test + public void testAfterSetText() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { + final Constructor markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class); + markerSpanConstructor.setAccessible(true); + + final Editable editable = new SpannableStringBuilder("Lorem Ipsum Dolor \nSit Amet"); + editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new URLSpan(""), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + final TextView textView = new TextView(ApplicationProvider.getApplicationContext()); + textView.setText(editable); + + assertEquals(3, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length); + + final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> { + // Do nothing... + }); + plugin.afterSetText(textView); + + assertEquals(0, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length); + } + + @Test + @SuppressWarnings({"unchecked", "ConstantConditions"}) + public void testGetClickableSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getClickableSpans", SpannableBuilder.class, int.class, int.class); + m.setAccessible(true); + + final Object firstClickableSpan = new URLSpan(""); + final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), ""); + final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet"); + spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + List clickableSpans; + + clickableSpans = (List) m.invoke(null, spannable, 0, 0); + assertEquals(0, clickableSpans.size()); + + clickableSpans = (List) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1); + assertEquals(0, clickableSpans.size()); + + clickableSpans = (List) m.invoke(null, spannable, 0, 5); + assertEquals(0, clickableSpans.size()); + + clickableSpans = (List) m.invoke(null, spannable, 0, spannable.length()); + assertEquals(2, clickableSpans.size()); + assertEquals(firstClickableSpan, clickableSpans.get(0).what); + assertEquals(secondClickableSpan, clickableSpans.get(1).what); + + clickableSpans = (List) m.invoke(null, spannable, 0, 17); + assertEquals(1, clickableSpans.size()); + assertEquals(firstClickableSpan, clickableSpans.get(0).what); + + clickableSpans = (List) m.invoke(null, spannable, 12, 22); + assertEquals(1, clickableSpans.size()); + assertEquals(secondClickableSpan, clickableSpans.get(0).what); + + clickableSpans = (List) m.invoke(null, spannable, 9, 20); + assertEquals(2, clickableSpans.size()); + assertEquals(firstClickableSpan, clickableSpans.get(0).what); + assertEquals(secondClickableSpan, clickableSpans.get(1).what); + } + + @Test + @SuppressWarnings({"unchecked", "ConstantConditions"}) + public void testFindFreeRanges() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("findFreeRanges", SpannableBuilder.class, int.class, int.class); + m.setAccessible(true); + + final Object firstClickableSpan = new URLSpan(""); + final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), ""); + final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet"); + spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + List> freeRanges; + + freeRanges = (List>) m.invoke(null, spannable, 0, 0); + assertEquals(0, freeRanges.size()); + + freeRanges = (List>) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1); + assertEquals(0, freeRanges.size()); + + freeRanges = (List>) m.invoke(null, spannable, 0, 6); + assertEquals(1, freeRanges.size()); + assertEquals(0, (int) freeRanges.get(0).getLower()); + assertEquals(6, (int) freeRanges.get(0).getUpper()); + + freeRanges = (List>) m.invoke(null, spannable, 0, 6); + assertEquals(1, freeRanges.size()); + assertEquals(0, (int) freeRanges.get(0).getLower()); + assertEquals(6, (int) freeRanges.get(0).getUpper()); + + freeRanges = (List>) m.invoke(null, spannable, 3, 15); + assertEquals(2, freeRanges.size()); + assertEquals(3, (int) freeRanges.get(0).getLower()); + assertEquals(6, (int) freeRanges.get(0).getUpper()); + assertEquals(11, (int) freeRanges.get(1).getLower()); + assertEquals(15, (int) freeRanges.get(1).getUpper()); + + freeRanges = (List>) m.invoke(null, spannable, 0, spannable.length()); + assertEquals(3, freeRanges.size()); + assertEquals(0, (int) freeRanges.get(0).getLower()); + assertEquals(6, (int) freeRanges.get(0).getUpper()); + assertEquals(11, (int) freeRanges.get(1).getLower()); + assertEquals(19, (int) freeRanges.get(1).getUpper()); + assertEquals(22, (int) freeRanges.get(2).getLower()); + assertEquals(27, (int) freeRanges.get(2).getUpper()); + } +} From 720390686feed06f0f8064e52a263ba0286d6c97 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 19:50:31 +0200 Subject: [PATCH 12/23] Refactor links in checkboxes and add unit tests Signed-off-by: Stefan Niedermann --- .../plugins/ToggleableTaskListPlugin.java | 37 ++++++++----------- .../plugins/ToggleableTaskListPluginTest.java | 22 ++++++----- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index 77411392..c068ed74 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -16,8 +16,9 @@ import org.commonmark.node.Paragraph; import org.commonmark.node.SoftLineBreak; import org.commonmark.node.Text; -import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; @@ -27,6 +28,7 @@ import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.SpanFactory; import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.SpannableBuilder.Span; import io.noties.markwon.ext.tasklist.TaskListItem; import io.noties.markwon.ext.tasklist.TaskListProps; import io.noties.markwon.ext.tasklist.TaskListSpan; @@ -56,6 +58,7 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { /** * Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step. + * The {@link ToggleMarkerSpan} are different from {@link TaskListSpan}s as they will stop on nested tasks instead of spanning the whole tasks including its subtasks. */ @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { @@ -107,19 +110,19 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { /** - * Adds for each {@link ToggleMarkerSpan} and actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. + * Adds for each symbolic {@link ToggleMarkerSpan} an actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s. */ @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { super.afterRender(node, visitor); - final List markerSpans = getSortedToggleMarkerSpans(visitor.builder()); + final List markerSpans = getSortedSpans(visitor.builder(), ToggleMarkerSpan.class, 0, visitor.builder().length()); for (int position = 0; position < markerSpans.size(); position++) { - final SpannableBuilder.Span markerSpan = markerSpans.get(position); + final Span markerSpan = markerSpans.get(position); final int start = markerSpan.start; final int end = markerSpan.end; - final List> freeRanges = findFreeRanges(visitor.builder(), start, end); + final Collection> freeRanges = findFreeRanges(visitor.builder(), start, end); for (Range freeRange : freeRanges) { visitor.builder().setSpan( new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position), @@ -145,13 +148,13 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { * @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is not taken for a {@link ClickableSpan}. */ @NonNull - private static List> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) { + private static Collection> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) { final List> freeRanges; - final List clickableSpans = getClickableSpans(builder, start, end); + final List clickableSpans = getSortedSpans(builder, ClickableSpan.class, start, end); if (clickableSpans.size() > 0) { - freeRanges = new ArrayList<>(clickableSpans.size()); + freeRanges = new LinkedList<>(); int from = start; - for (SpannableBuilder.Span clickableSpan : clickableSpans) { + for (Span clickableSpan : clickableSpans) { final int clickableStart = clickableSpan.start; final int clickableEnd = clickableSpan.end; if (from != clickableStart) { @@ -174,22 +177,12 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { } /** - * @return a {@link List} of {@link ToggleMarkerSpan}s, sorted ascending by the span start. + * @return a {@link List} of {@link Span}s holding {@param type}s, sorted ascending by the span start. */ - @NonNull - private static List getSortedToggleMarkerSpans(@NonNull SpannableBuilder builder) { - return builder.getSpans(0, builder.length()) - .stream() - .filter(span -> span.what instanceof ToggleMarkerSpan) - .sorted((o1, o2) -> o1.start - o2.start) - .collect(Collectors.toList()); - } - - @NonNull - private static List getClickableSpans(@NonNull SpannableBuilder builder, int start, int end) { + private static List getSortedSpans(@NonNull SpannableBuilder builder, @NonNull Class type, int start, int end) { return builder.getSpans(start, end) .stream() - .filter(span -> span.what instanceof ClickableSpan) + .filter(span -> type.isInstance(span.what)) .sorted((o1, o2) -> o1.start - o2.start) .collect(Collectors.toList()); } diff --git a/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java index 823e1e13..d864aea4 100644 --- a/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java +++ b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java @@ -5,6 +5,7 @@ import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.URLSpan; import android.util.Range; import android.widget.TextView; @@ -111,41 +112,44 @@ public class ToggleableTaskListPluginTest extends TestCase { @Test @SuppressWarnings({"unchecked", "ConstantConditions"}) - public void testGetClickableSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getClickableSpans", SpannableBuilder.class, int.class, int.class); + public void testGetSortedSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getSortedSpans", SpannableBuilder.class, Class.class, int.class, int.class); m.setAccessible(true); final Object firstClickableSpan = new URLSpan(""); final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), ""); + final Object unclickableSpan = new ForegroundColorSpan(android.R.color.white); + final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet"); spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan(unclickableSpan, 3, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); List clickableSpans; - clickableSpans = (List) m.invoke(null, spannable, 0, 0); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 0, 0); assertEquals(0, clickableSpans.size()); - clickableSpans = (List) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, spannable.length() - 1, spannable.length() - 1); assertEquals(0, clickableSpans.size()); - clickableSpans = (List) m.invoke(null, spannable, 0, 5); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 0, 5); assertEquals(0, clickableSpans.size()); - clickableSpans = (List) m.invoke(null, spannable, 0, spannable.length()); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 0, spannable.length()); assertEquals(2, clickableSpans.size()); assertEquals(firstClickableSpan, clickableSpans.get(0).what); assertEquals(secondClickableSpan, clickableSpans.get(1).what); - clickableSpans = (List) m.invoke(null, spannable, 0, 17); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 0, 17); assertEquals(1, clickableSpans.size()); assertEquals(firstClickableSpan, clickableSpans.get(0).what); - clickableSpans = (List) m.invoke(null, spannable, 12, 22); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 12, 22); assertEquals(1, clickableSpans.size()); assertEquals(secondClickableSpan, clickableSpans.get(0).what); - clickableSpans = (List) m.invoke(null, spannable, 9, 20); + clickableSpans = (List) m.invoke(null, spannable, ClickableSpan.class, 9, 20); assertEquals(2, clickableSpans.size()); assertEquals(firstClickableSpan, clickableSpans.get(0).what); assertEquals(secondClickableSpan, clickableSpans.get(1).what); From 96dd4f17a72e11a2f59ac413fa75157a8abf7e7c Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 20:38:15 +0200 Subject: [PATCH 13/23] Remove dead code Signed-off-by: Stefan Niedermann --- .../markdown/markwon/plugins/ToggleableTaskListPlugin.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java index c068ed74..7aee5a27 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -41,8 +41,6 @@ import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan; */ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { - private static final String TAG = ToggleableTaskListPlugin.class.getSimpleName(); - @NonNull private final AtomicBoolean enabled = new AtomicBoolean(true); @NonNull From 95e9b472abc05129c9e4ee79cfe3523b150b10c3 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 13 Jun 2021 20:50:56 +0200 Subject: [PATCH 14/23] update changelog Signed-off-by: Stefan Niedermann --- fastlane/metadata/android/en-US/changelogs/3004010.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/changelogs/3004010.txt b/fastlane/metadata/android/en-US/changelogs/3004010.txt index 199b9cc3..f3ee0017 100644 --- a/fastlane/metadata/android/en-US/changelogs/3004010.txt +++ b/fastlane/metadata/android/en-US/changelogs/3004010.txt @@ -1,3 +1,4 @@ v3.4.10 -- Open trashbin of files app instead of Web UI (#238) \ No newline at end of file +- Open trashbin of files app instead of Web UI (#238) +- Make links in checkbox list items clickable \ No newline at end of file From ca59964994c911f22480416d928d2eef38044f85 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 14 Jun 2021 09:53:41 +0200 Subject: [PATCH 15/23] Update FAQ Signed-off-by: Stefan Niedermann --- FAQ.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/FAQ.md b/FAQ.md index 60797b52..17391397 100644 --- a/FAQ.md +++ b/FAQ.md @@ -27,17 +27,45 @@ Sorry. There are so many different environments, that it is impossible for us to First of all make sure you have updated to and tried with the latest available versions of both, this app and the [Notes server app](https://apps.nextcloud.com/apps/notes). -In case you receive a `NextcloudApiNotRespondingException`, try to disable the battery optimization for both apps. -In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android. +### `NextcloudApiNotRespondingException` + +Try to disable the battery "optimization" for both apps. Some manufacturers prevent the app from communicating with the Nextcloud Android properly. +This is a [known issue of the SingleSignOn mechanism](https://github.com/nextcloud/Android-SingleSignOn#troubleshooting) which we only can work around but not solve on our side. + +### `UnknownErrorException: Read timed out` + +This issue is caused by a connection time out. This can be the case if there are infrastructural or environmental problems (like a misconfigured server or a bad network connection). +Probably you will experience it when importing an account, because at this moment, all your Notes will getting downloaded at once. Given you have a lots of notes, this might take longer than the connection is available. +Further synchronizations are usually not causing this issue, because the Notes app tries to synchronize only *changed* notes after the first import. +If your notes are not ten thousands of characters long, it is very unlikely that this causes a connection timeout. + +We plan to improve the import of an account and make it more reliable by [fetching notes step by step](https://github.com/stefan-niedermann/nextcloud-notes/issues/761#issuecomment-836989421) in a future release. +Until then you can as a workaround for the first import try to +1. move all your notes to a different folder on your Nextcloud instance +2. import your account on your smartphone +3. put your notes back to the original folder step by step and sync everytime you put some notes back + +### `NextcloudFilesAppAccountNotFoundException` + +We are not yet sure what exactly causes this issue, but investigate it by [adding more debug logs to recent versions](https://github.com/stefan-niedermann/nextcloud-notes/issues/1256#issuecomment-859505153). In theory this might happen if an already imported account has been deleted in the Nextcloud app. +As a workaround you can remove the account (or clear the storage of the app as described below if you can't access the account manager anymore) and import it again. + +### `TokenMismatchException` and all others + +In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android. Not yet synchronized changes will be lost by performing this step. You can achieve this by navigating to ``` Android settings - ↳ Apps - ↳ Nextcloud / Notes - ↳ Storage - ↳ Clear storage + ↓ + Apps + ↓ +Nextcloud / Notes + ↓ + Storage + ↓ + Clear storage ``` Then set up your account in the Nextcloud Android app again and import the configured account in the Nextcloud Notes Android app. From 806914bb1671095a3087f303fe495b6b7ea12177 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 08:04:17 +0000 Subject: [PATCH 16/23] Bump mockito-core from 3.11.0 to 3.11.1 Bumps [mockito-core](https://github.com/mockito/mockito) from 3.11.0 to 3.11.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.11.0...v3.11.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- app/build.gradle | 2 +- markdown/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 87b2002b..1cc8c676 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,7 +112,7 @@ dependencies { testImplementation 'androidx.test:core:1.3.0' testImplementation 'androidx.arch.core:core-testing:2.1.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:3.11.0' + testImplementation 'org.mockito:mockito-core:3.11.1' testImplementation 'org.robolectric:robolectric:4.5.1' implementation fileTree(dir: 'libs', include: ['*.jar']) diff --git a/markdown/build.gradle b/markdown/build.gradle index a9107750..71ae490a 100644 --- a/markdown/build.gradle +++ b/markdown/build.gradle @@ -60,6 +60,6 @@ dependencies { testImplementation 'androidx.test:core:1.3.0' testImplementation 'androidx.arch.core:core-testing:2.1.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:3.11.0' + testImplementation 'org.mockito:mockito-core:3.11.1' testImplementation 'org.robolectric:robolectric:4.5.1' } \ No newline at end of file From b06c109afb837bccffc4ce3acc9134b8739f5185 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Tue, 15 Jun 2021 12:08:15 +0200 Subject: [PATCH 17/23] #1256 Provide backup and repair steps Signed-off-by: Stefan Niedermann --- .../owncloud/notes/main/MainActivity.java | 68 ++++++++++++++----- .../owncloud/notes/main/MainViewModel.java | 4 ++ app/src/main/res/values/strings.xml | 3 + 3 files changed, 57 insertions(+), 18 deletions(-) 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 49f4a3e7..bbd704bf 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 @@ -15,9 +15,11 @@ import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; import android.widget.LinearLayout; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.SearchView; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -50,9 +52,11 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper; import java.net.HttpURLConnection; import java.util.Collection; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import java.util.stream.Collectors; import it.niedermann.owncloud.notes.LockedActivity; @@ -90,6 +94,7 @@ import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.model.NoteClickListener; import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule; import it.niedermann.owncloud.notes.shared.util.NoteUtil; +import it.niedermann.owncloud.notes.shared.util.ShareUtil; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.O; @@ -175,25 +180,52 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A try { final Account account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name); runOnUiThread(() -> mainViewModel.postCurrentAccount(account)); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + } catch (NextcloudFilesAppAccountNotFoundException e) { // Verbose log output for https://github.com/stefan-niedermann/nextcloud-notes/issues/1256 - final Throwable exception; - if (e instanceof NextcloudFilesAppAccountNotFoundException) { - final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext()); - final StringBuilder ssoPreferencesString = new StringBuilder() - .append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n") - .append("\n") - .append("SSO SharedPreferences: ").append("\n"); - for (Map.Entry entry : ssoPreferences.getAll().entrySet()) { - ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); - } - ssoPreferencesString.append("\n") - .append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList()))); - exception = new RuntimeException(((NextcloudFilesAppAccountNotFoundException) e).getMessage(), new RuntimeException(ssoPreferencesString.toString(), e)); - } else { - exception = e; - } - runOnUiThread(() -> ExceptionDialogFragment.newInstance(exception).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + runOnUiThread(() -> new AlertDialog.Builder(this) + .setTitle(NextcloudFilesAppAccountNotFoundException.class.getSimpleName()) + .setMessage(R.string.backup_and_repair) + .setPositiveButton(R.string.simple_repair, (a, b) -> { + executor.submit(() -> { + for (Account account : mainViewModel.getAccounts()) { + SingleAccountHelper.setCurrentAccount(this, account.getAccountName()); + runOnUiThread(this::recreate); + break; + } + }); + }) + .setNegativeButton(R.string.simple_backup, (a, b) -> { + executor.submit(() -> { + final List modifiedNotes = new LinkedList<>(); + for (Account account : mainViewModel.getAccounts()) { + modifiedNotes.addAll(mainViewModel.getLocalModifiedNotes(account.getId())); + } + if (modifiedNotes.size() == 1) { + final Note note = modifiedNotes.get(0); + ShareUtil.openShareDialog(this, note.getTitle(), note.getContent()); + } else { + ShareUtil.openShareDialog(this, + getResources().getQuantityString(R.plurals.share_multiple, modifiedNotes.size(), modifiedNotes.size()), + mainViewModel.collectNoteContents(modifiedNotes.stream().map(Note::getId).collect(Collectors.toList()))); + } + }); + }) + .setNeutralButton(android.R.string.cancel, (a, b) -> { + final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext()); + final StringBuilder ssoPreferencesString = new StringBuilder() + .append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n") + .append("\n") + .append("SSO SharedPreferences: ").append("\n"); + for (Map.Entry entry : ssoPreferences.getAll().entrySet()) { + ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); + } + ssoPreferencesString.append("\n") + .append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList()))); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(new RuntimeException(e.getMessage(), new RuntimeException(ssoPreferencesString.toString(), e))).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + }) + .show()); + } catch (NoCurrentAccountSelectedException e) { + runOnUiThread(() -> ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } 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 716bbd79..92790bc4 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 @@ -594,6 +594,10 @@ public class MainViewModel extends AndroidViewModel { repo.createOrUpdateSingleNoteWidgetData(data); } + public List getLocalModifiedNotes(long accountId) { + return repo.getLocalModifiedNotes(accountId); + } + public LiveData getAccountsCount() { return repo.countAccounts$(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f5b9e52..1fa41edf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -295,4 +295,7 @@ You have to be connected to the internet in order to add an account. Next Previous + Backup + Repair + We recommend to backup all unsynchronized notes and then try to repair the setup. From 6848a82d6f3467e8f4a0095d50087120f330f506 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Tue, 15 Jun 2021 12:09:33 +0200 Subject: [PATCH 18/23] update changelog Signed-off-by: Stefan Niedermann --- fastlane/metadata/android/en-US/changelogs/3004010.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/3004010.txt b/fastlane/metadata/android/en-US/changelogs/3004010.txt index f3ee0017..92da1c45 100644 --- a/fastlane/metadata/android/en-US/changelogs/3004010.txt +++ b/fastlane/metadata/android/en-US/changelogs/3004010.txt @@ -1,4 +1,3 @@ -v3.4.10 - -- Open trashbin of files app instead of Web UI (#238) -- Make links in checkbox list items clickable \ No newline at end of file +- 🗑 Open trashbin of files app instead of Web UI (#238) +- ✅ Make links in checkbox list items clickable +- 🐞 NextcloudFilesAppAccountNotFoundException - Trying to provide backup & repair steps (#1256) \ No newline at end of file From 2a2ed1839b5be6d7e19c0ae05ff3a0545c46c61b Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Tue, 15 Jun 2021 12:10:03 +0200 Subject: [PATCH 19/23] version bump to 3.4.10 Signed-off-by: Stefan Niedermann --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1cc8c676..1fe507ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "it.niedermann.owncloud.notes" minSdkVersion 21 targetSdkVersion 30 - versionCode 3004009 - versionName "3.4.9" + versionCode 3004010 + versionName "3.4.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions { annotationProcessorOptions { From 0857c2e7132aa86951edf3430a06be769cbf71e1 Mon Sep 17 00:00:00 2001 From: ScottieKim Date: Thu, 17 Jun 2021 21:39:16 +0900 Subject: [PATCH 20/23] Update: Back button error #1266 solved --- .../java/it/niedermann/owncloud/notes/main/MainActivity.java | 2 ++ 1 file changed, 2 insertions(+) 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 bbd704bf..3d9e2ff0 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 @@ -776,6 +776,8 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A public void onBackPressed() { if (activityBinding.toolbar.getVisibility() == VISIBLE) { updateToolbars(true); + } else if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } From bb92c9aeda9d8590888a9fa31367e1a10bdecae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Jun 2021 13:11:20 +0000 Subject: [PATCH 21/23] Bump fragment from 1.3.4 to 1.3.5 Bumps fragment from 1.3.4 to 1.3.5. --- updated-dependencies: - dependency-name: androidx.fragment:fragment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1fe507ec..6218d391 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,7 +86,7 @@ dependencies { // Android X implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'androidx.fragment:fragment:1.3.4' + implementation 'androidx.fragment:fragment:1.3.5' implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' From 35605e4a0ae0dd27adeceec568183ca728d5d428 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 17 Jun 2021 16:30:59 +0200 Subject: [PATCH 22/23] Minor code improvements Signed-off-by: Stefan Niedermann --- .../owncloud/notes/edit/NoteEditFragment.java | 7 +++--- .../notes/shared/util/DisplayUtils.java | 23 ++++--------------- 2 files changed, 7 insertions(+), 23 deletions(-) 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 11e1ec4f..83c8eb1a 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 @@ -16,7 +16,6 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; -import android.view.ViewParent; import android.view.inputmethod.InputMethodManager; import android.widget.ScrollView; @@ -184,10 +183,10 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { binding.editContent.removeTextChangedListener(textWatcher); cancelTimers(); - final ViewGroup parentView = requireActivity().findViewById(android.R.id.content); - if(parentView != null && parentView.getChildCount() > 0){ + final ViewGroup parentView = requireActivity().findViewById(android.R.id.content); + if (parentView != null && parentView.getChildCount() > 0) { keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView.getChildAt(0)); - }else { + } else { keyboardShown = false; } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index ffea98b1..06cbf57d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -2,34 +2,21 @@ package it.niedermann.owncloud.notes.shared.util; import android.content.Context; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.Rect; import android.os.Build; -import android.text.Spannable; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.MetricAffectingSpan; import android.util.TypedValue; import android.view.View; import android.view.WindowInsets; -import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import java.util.Collection; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import it.niedermann.android.util.ColorUtil; -import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount; @@ -69,21 +56,19 @@ public class DisplayUtils { */ public static boolean isSoftKeyboardVisible(@NonNull View parentView) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView); - if(insets != null){ + final WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView); + if (insets != null) { return insets.isVisible(WindowInsets.Type.ime()); } } - //Fall Back to workaround //Arbitrary keyboard height final int defaultKeyboardHeightDP = 100; final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); final Rect rect = new Rect(); - - int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); + final int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); parentView.getWindowVisibleDisplayFrame(rect); - int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); + final int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); return heightDiff >= estimatedKeyboardHeight; } } From 0b10d98cba7dfb8848a98199b4a6f603d17b9219 Mon Sep 17 00:00:00 2001 From: Nextcloud bot Date: Thu, 17 Jun 2021 04:06:47 +0000 Subject: [PATCH 23/23] [tx-robot] updated from transifex --- app/src/main/res/values-cs-rCZ/strings.xml | 3 +++ app/src/main/res/values-de/strings.xml | 3 +++ app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fi-rFI/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hu-rHU/strings.xml | 15 ++++++++++++++- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 3 +++ app/src/main/res/values-pt-rBR/strings.xml | 3 +++ app/src/main/res/values-sl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 3 +++ app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 3 +++ app/src/main/res/values-zh-rHK/strings.xml | 3 +++ 16 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 84a036de..912efab9 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -259,4 +259,7 @@ Abyste mohli přidat účet je třeba, abyste byli připojení k Internetu. Další Předchozí + Zazálohovat + Opravit + Doporučujeme zazálohovat veškeré nesesynchronizované poznámky a pak se pokusit opravit nastavení. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index fc6cf276..1ed1f92a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -249,4 +249,7 @@ Sie müssen mit dem Internet verbunden sein, um ein Konto hinzufügen zu können. Weiter Vorheriges + Sicherung + Reparieren + Wir empfehlen, alle unsynchronisierten Notizen zu sichern und dann zu reparieren. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f323d4c8..4e77a61f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -249,4 +249,4 @@ Tienes que estar conectado a internet para poder añadir una cuenta. Siguiente Anterior - + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 4d387996..19b244ab 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -249,4 +249,4 @@ Kontu bat gehitzeko internetera konektatuta egon behar zara. Hurrengoa Aurrekoa - + diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 48ec1482..38bc538b 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -245,4 +245,4 @@ Sinun tulee olla yhteydessä internetiin, jotta voit lisätä tilin. Seuraava Edellinen - + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 435eb1be..64485bc5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -12,6 +12,7 @@ Méthode de tri Annuler Modifier + Supprimer Enregistrer À propos Lien diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 4b4eb9ff..d1adce14 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -6,12 +6,14 @@ Minden jegyzet Kedvencek Új jegyzet + Üdvözli a %1$s Beállítások Törölt jegyzetek Keresés Rendezési mód Mégse Szerkesztés + Eltávolítás Mentés Névjegy Hivatkozás @@ -50,6 +52,7 @@ Szinkronizálás sikertelen: %1$s Szinkronizálás sikertelen Nincs hálózati kapcsolat + A kiszolgáló karbantartási módban van Ismeretlen hiba történt. Verzió @@ -184,7 +187,7 @@ ```javascript Környezetfüggő formázás A Jegyzetek alkalmazás egyik célkitűzése, hogy egyszerű legyen, és ne vonja el a figyelmet. Viszont Markdownnal formázhatja a szövegeket. A lent említett példák némelyikénél rövidítéseket is használhat, így anélkül formázhatja meg a jegyzeteket, hogy beírná a lenti kódokat. - Csak válasszon ki egy szövegtartományt, vagy érintse meg a kurzort bármely helyen, és megjelenik egy előugró menü, amely az alapértelmezett bejegyzések mellett %1$s, %2$s, %3$s tartalmaz olyan bejegyzéseket, mint %4$s vagy %5$s. + Csak válasszon ki egy szövegtartományt, vagy érintse meg bárhol a kurzort, és megjelenik egy előugró menü, amely az alapértelmezett %1$s, %2$s, %3$s bejegyzések mellett ezeket is tartalmazza: %4$s, %5$s. Szöveg Nagyon egyszerűen írhat Markdownnal %1$sfélkövéren%1$s, valamint %2$sdőlten%2$s. Át is %3$shúzhat%3$s szavakat, valamint [hivatkozhat a Nextcloudra](https://nextcloud.com). @@ -235,5 +238,15 @@ Hálózati beállítások Nincs még fiók beállítva Még egyetlen más fiókot sem állított be. + Válasszon fiókot Környezetfüggő formázási felbukkanó menü + + A(z) %1$s fiók eltávolítása véglegesen töröl egy nem szinkronizált változtatást. + A(z) %1$s fiók eltávolítása véglegesen töröl %2$d nem szinkronizált változtatást. + + %1$s eltávolítása + + Internetkapcsolatra van szükség, hogy fiókot adjon hozzá. + Következő + Előző diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dc800393..ed87afbb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -249,4 +249,4 @@ Devi essere connesso a Internet per aggiungere un account. Successivo Precedente - + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 45787517..f1ef2fcb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -249,4 +249,4 @@ Je moet verbonden zijn met het Internet om een account toe te voegen. Volgende Vorige - + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 62e415df..e6e1f2b8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -259,4 +259,7 @@ Aby dodać konto, musisz mieć połączenie z Internetem. Następna Poprzednia + Kopia zapasowa + Naprawa + Zalecane wykonanie kopii zapasowej wszystkich niezsynchronizowanych notatek, a następnie próbę naprawy konfiguracji. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6955bf15..23f7ed9a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -249,4 +249,7 @@ Você deve estar conectado à Internet para adicionar uma conta. Próximo Anterior + Backup + Reparar + Recomendamos fazer backup de todas as notas não sincronizadas e, em seguida, tentar reparar a configuração. diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index aa77d43e..afdc562b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -259,4 +259,4 @@ Za dodajanje računa mora biti vzpostavljena povezava z omrežjem. Naslednje Predhodno - + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ecbe25aa..1f728e81 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -249,4 +249,7 @@ Bir hesap ekleyebilmeniz için çalışan bir İnternet bağlantınız olmalı. Sonraki Önceki + Yedekle + Onar + Eşitlenmemiş tüm notları yedekledikten sonra kurulumu onarmayı denemeniz önerilir. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 69f72d24..b57ce4c7 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -226,4 +226,4 @@ Bạn phải có kết nối internet để thêm tài khoản. Tiếp Trước - + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 88981861..ba190366 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -244,4 +244,7 @@ 要添加账号,你必须连接到互联网。 下一则 上一则 + 备份 + 修复 + 我们建议备份所有未同步的笔记,然后尝试修复安装。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 0b9f7615..b97ec305 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -244,4 +244,7 @@ 您必須連線到互聯網才能新增賬戶。 下一步 上一個 + 備份 + 修復 + 我們建議備份所有未同步的筆記,然後嘗試修復設置。