diff --git a/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java
index 1d3f016b..91d666ce 100644
--- a/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java
+++ b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java
@@ -125,6 +125,20 @@ public class SimpleMarkdownParser {
return text;
}
};
+ public final static SmpFilter FILTER_H_TO_SUP = new SmpFilter() {
+ @Override
+ public String filter(String text) {
+ text = text
+ .replace("
", "")
+ .replace("
", "")
+ .replace("", "")
+ .replace("
", "")
+ .replace("", "")
+ .replace("
", "")
+ ;
+ return text;
+ }
+ };
public final static SmpFilter FILTER_NONE = new SmpFilter() {
@Override
public String filter(String text) {
diff --git a/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java b/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java
index 3371f4b8..7ba77cea 100644
--- a/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java
+++ b/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java
@@ -11,12 +11,14 @@
package net.gsantner.opoc.ui;
import android.app.Activity;
+import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.ColorInt;
+import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
@@ -24,10 +26,14 @@ import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableString;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.util.Pair;
import android.view.Gravity;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
@@ -58,16 +64,23 @@ public class SearchOrCustomTextDialog {
public static class DialogOptions {
public Callback.a1 callback;
- public List extends CharSequence> data = new ArrayList<>();
- public List extends CharSequence> highlightData = new ArrayList<>();
- public List iconsForData = new ArrayList<>();
+ public Callback.a2 withPositionCallback;
+ public List extends CharSequence> data;
+ public List extends CharSequence> highlightData;
+ public List iconsForData;
public String messageText = "";
+ public String defaultText = "";
public boolean isSearchEnabled = true;
public boolean isDarkDialog = false;
public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT;
public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT;
public int gravity = Gravity.NO_GRAVITY;
public int searchInputType = 0;
+ public boolean searchIsRegex = false;
+ public Callback.a1 highlighter;
+ public String extraFilter = null;
+
+ public Callback.a0 neutralButtonCallback = null;
@ColorInt
public int textColor = 0xFF000000;
@@ -76,77 +89,122 @@ public class SearchOrCustomTextDialog {
@StringRes
public int cancelButtonText = android.R.string.cancel;
@StringRes
+ public int neutralButtonText = 0;
+ @StringRes
public int okButtonText = android.R.string.ok;
@StringRes
- public int titleText = android.R.string.untitled;
+ public int titleText = 0;
@StringRes
public int searchHintText = android.R.string.search_go;
}
+ private static class WithPositionAdapter extends ArrayAdapter> {
+
+ final LayoutInflater mInflater;
+ final @LayoutRes
+ int mLayout;
+ final DialogOptions dopt;
+ final List> filteredItems;
+ final Pattern extraPattern;
+
+ WithPositionAdapter(Context context, @LayoutRes int layout, List> filteredItems, DialogOptions dopt) {
+ super(context, layout, filteredItems);
+ mInflater = LayoutInflater.from(context);
+ mLayout = layout;
+ this.dopt = dopt;
+ this.filteredItems = filteredItems;
+ extraPattern = dopt.extraFilter == null ? null : Pattern.compile(dopt.extraFilter);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int pos, @Nullable View convertView, @NonNull ViewGroup parent) {
+ final Pair item = getItem(pos);
+ final String text = item.first;
+ final int posInOriginalList = item.second;
+
+ final TextView textView;
+ if (convertView == null) {
+ textView = (TextView) mInflater.inflate(mLayout, parent, false);
+ } else {
+ textView = (TextView) convertView;
+ }
+
+ if (posInOriginalList >= 0 && dopt.iconsForData != null && posInOriginalList < dopt.iconsForData.size() && dopt.iconsForData.get(posInOriginalList) != 0) {
+ textView.setCompoundDrawablesWithIntrinsicBounds(dopt.iconsForData.get(posInOriginalList), 0, 0, 0);
+ textView.setCompoundDrawablePadding(32);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ textView.setCompoundDrawableTintList(ColorStateList.valueOf(dopt.isDarkDialog ? Color.WHITE : Color.BLACK));
+ }
+ } else {
+ textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+
+ if (dopt.highlightData != null) {
+ final boolean hl = dopt.highlightData.contains(text);
+ textView.setTextColor(hl ? dopt.highlightColor : dopt.textColor);
+ textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL);
+ }
+
+ if (dopt.highlighter != null) {
+ Spannable s = new SpannableString(text);
+ dopt.highlighter.callback(s);
+ textView.setText(s);
+ } else {
+ textView.setText(text);
+ }
+
+ return textView;
+ }
+
+ @Override
+ public Filter getFilter() {
+ return new Filter() {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void publishResults(final CharSequence constraint, final FilterResults results) {
+ filteredItems.clear();
+ filteredItems.addAll((List>) results.values);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ protected FilterResults performFiltering(final CharSequence constraint) {
+ final ArrayList> resList = new ArrayList<>();
+
+ if (dopt.data != null) {
+ final String fil = constraint.toString();
+ final boolean emptySearch = fil.isEmpty();
+ for (int i = 0; i < dopt.data.size(); i++) {
+ final CharSequence str = dopt.data.get(i);
+ final boolean matchExtra = (extraPattern == null) || extraPattern.matcher(str).find();
+ final boolean matchNormal = str.toString().toLowerCase(Locale.getDefault()).contains(fil.toLowerCase(Locale.getDefault()));
+ final boolean matchRegex = dopt.searchIsRegex && (str.toString().matches(fil));
+ if (matchExtra && (matchNormal || matchRegex || emptySearch)) {
+ resList.add(new Pair<>(str, i));
+ }
+ }
+ }
+
+ final FilterResults res = new FilterResults();
+ res.values = resList;
+ res.count = resList.size();
+ return res;
+ }
+ };
+ }
+ }
+
public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activity, final DialogOptions dopt) {
- final List allItems = new ArrayList<>(dopt.data);
- final List filteredItems = new ArrayList<>(allItems);
+ final List> filteredItems = new ArrayList<>();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, dopt.isDarkDialog
? android.support.v7.appcompat.R.style.Theme_AppCompat_Dialog
: android.support.v7.appcompat.R.style.Theme_AppCompat_Light_Dialog
);
-
- final ArrayAdapter listAdapter = new ArrayAdapter(activity, android.R.layout.simple_list_item_1, filteredItems) {
- @NonNull
- @Override
- public View getView(int pos, @Nullable View convertView, @NonNull ViewGroup parent) {
- TextView textView = (TextView) super.getView(pos, convertView, parent);
- String text = textView.getText().toString();
-
- int posInOriginalList = dopt.data.indexOf(text);
- if (posInOriginalList >= 0 && dopt.iconsForData != null && posInOriginalList < dopt.iconsForData.size() && dopt.iconsForData.get(posInOriginalList) != 0) {
- textView.setCompoundDrawablesWithIntrinsicBounds(dopt.iconsForData.get(posInOriginalList), 0, 0, 0);
- textView.setCompoundDrawablePadding(32);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- textView.setCompoundDrawableTintList(ColorStateList.valueOf(dopt.isDarkDialog ? Color.WHITE : Color.BLACK));
- }
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
- }
-
- boolean hl = dopt.highlightData.contains(text);
- textView.setTextColor(hl ? dopt.highlightColor : dopt.textColor);
- textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL);
-
- return textView;
- }
-
- @Override
- public Filter getFilter() {
- return new Filter() {
- @SuppressWarnings("unchecked")
- @Override
- protected void publishResults(final CharSequence constraint, final FilterResults results) {
- filteredItems.clear();
- filteredItems.addAll((List) results.values);
- notifyDataSetChanged();
- }
-
- @Override
- protected FilterResults performFiltering(final CharSequence constraint) {
- final FilterResults res = new FilterResults();
- final ArrayList resList = new ArrayList<>();
- final String fil = constraint.toString();
-
- for (final CharSequence str : allItems) {
- if ("".equals(fil) || str.toString().toLowerCase(Locale.getDefault()).contains(fil.toLowerCase(Locale.getDefault()))) {
- resList.add(str);
- }
- }
- res.values = resList;
- res.count = resList.size();
- return res;
- }
- };
- }
- };
+ final WithPositionAdapter listAdapter = new WithPositionAdapter(activity, android.R.layout.simple_list_item_1, filteredItems, dopt);
final AppCompatEditText searchEditText = new AppCompatEditText(activity);
+ searchEditText.setText(dopt.defaultText);
searchEditText.setSingleLine(true);
searchEditText.setMaxLines(1);
searchEditText.setTextColor(dopt.textColor);
@@ -174,24 +232,35 @@ public class SearchOrCustomTextDialog {
listView.setAdapter(listAdapter);
listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE);
linearLayout.setOrientation(LinearLayout.VERTICAL);
+
if (dopt.isSearchEnabled) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
int px = (int) (new ContextUtils(listView.getContext()).convertDpToPx(8));
lp.setMargins(px, px / 2, px, px / 2);
linearLayout.addView(searchEditText, lp);
}
+
final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0);
layoutParams.weight = 1;
linearLayout.addView(listView, layoutParams);
if (!TextUtils.isEmpty(dopt.messageText)) {
dialogBuilder.setMessage(dopt.messageText);
}
+
dialogBuilder.setView(linearLayout)
.setOnCancelListener(null)
.setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss());
+
+ if (dopt.neutralButtonCallback != null && dopt.neutralButtonText != 0) {
+ dialogBuilder.setNeutralButton(dopt.neutralButtonText, (dialogInterface, i) -> {
+ dopt.neutralButtonCallback.callback();
+ });
+ }
+
if (dopt.titleText != 0) {
dialogBuilder.setTitle(dopt.titleText);
}
+
if (dopt.isSearchEnabled) {
dialogBuilder.setPositiveButton(dopt.okButtonText, (dialogInterface, i) -> {
dialogInterface.dismiss();
@@ -205,7 +274,11 @@ public class SearchOrCustomTextDialog {
listView.setOnItemClickListener((parent, view, position, id) -> {
dialog.dismiss();
if (dopt.callback != null) {
- dopt.callback.callback(filteredItems.get(position).toString());
+ dopt.callback.callback(filteredItems.get(position).first);
+ }
+ if (dopt.withPositionCallback != null) {
+ final Pair item = filteredItems.get(position);
+ dopt.withPositionCallback.callback(item.first, item.second);
}
});
@@ -220,7 +293,6 @@ public class SearchOrCustomTextDialog {
return false;
});
-
Window w;
if ((w = dialog.getWindow()) != null && dopt.isSearchEnabled) {
w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
@@ -241,6 +313,9 @@ public class SearchOrCustomTextDialog {
if (dopt.isSearchEnabled) {
searchEditText.requestFocus();
}
+ if (dopt.defaultText != null) {
+ listAdapter.getFilter().filter(searchEditText.getText());
+ }
}
diff --git a/app/src/main/java/net/gsantner/opoc/util/Callback.java b/app/src/main/java/net/gsantner/opoc/util/Callback.java
index 7a3186e8..640b5cce 100644
--- a/app/src/main/java/net/gsantner/opoc/util/Callback.java
+++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java
@@ -37,6 +37,10 @@ public class Callback {
void callback(A arg1, B arg2, C arg3, D arg4, E arg5);
}
+ public interface b0 {
+ boolean callback();
+ }
+
public interface b1 {
boolean callback(A arg1);
}
@@ -56,4 +60,28 @@ public class Callback {
public interface b5 {
boolean callback(A arg1, B arg2, C arg3, D arg4, E arg5);
}
+
+ public interface s0 {
+ String callback();
+ }
+
+ public interface s1 {
+ String callback(A arg1);
+ }
+
+ public interface s2 {
+ String callback(A arg1, B arg2);
+ }
+
+ public interface s3 {
+ String callback(A arg1, B arg2, C arg3);
+ }
+
+ public interface s4 {
+ String callback(A arg1, B arg2, C arg3, D arg4);
+ }
+
+ public interface s5 {
+ String callback(A arg1, B arg2, C arg3, D arg4, E arg5);
+ }
}
diff --git a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java
index 141f1b47..cab0128d 100644
--- a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java
+++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java
@@ -893,7 +893,7 @@ public class ContextUtils {
public CharSequence filter(CharSequence src, int start, int end, Spanned dest, int dstart, int dend) {
if (src.length() < 1) return null;
char last = src.charAt(src.length() - 1);
- String illegal = "|\\?*<\":>+[]/'";
+ String illegal = "|\\?*<\":>[]/'";
if (illegal.indexOf(last) > -1) return src.subSequence(0, src.length() - 1);
return null;
}
@@ -935,7 +935,11 @@ public class ContextUtils {
ContentResolver cr = _context.getContentResolver();
mimeType = cr.getType(uri);
} else {
- String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String filename = uri.toString();
+ if (filename.endsWith(".jenc")) {
+ filename = filename.replace(".jenc", "");
+ }
+ String ext = MimeTypeMap.getFileExtensionFromUrl(filename);
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
// Try to guess if the recommended methods fail
diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java
index c8a2b6d3..4e96a8ec 100644
--- a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java
+++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java
@@ -409,9 +409,10 @@ public class FileUtils {
if (guess == null || guess.isEmpty()) {
guess = "*/*";
- int dot = file.getName().lastIndexOf(".") + 1;
- if (dot > 0 && dot < file.getName().length()) {
- switch (file.getName().substring(dot)) {
+ String filename = file.getName().replace(".jenc", "");
+ int dot = filename.lastIndexOf(".") + 1;
+ if (dot > 0 && dot < filename.length()) {
+ switch (filename.substring(dot)) {
case "md":
case "markdown":
case "mkd":
@@ -488,4 +489,16 @@ public class FileUtils {
ret[2] = (int) (diff / 1000) % 60; // sec
return ret;
}
+
+ public static String getHumanReadableByteCountSI(final long bytes) {
+ if (bytes < 1000) {
+ return String.format(Locale.getDefault(), "%d%s", bytes, "B");
+ } else if (bytes < 1000000) {
+ return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000f), "KB");
+ } else if (bytes < 1000000000) {
+ return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000000f), "GB");
+ } else {
+ return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000000000f), "TB");
+ }
+ }
}
diff --git a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java
index a8c564c6..08e36948 100644
--- a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java
+++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java
@@ -24,6 +24,7 @@ import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -150,6 +151,7 @@ public class NetworkUtils {
return performCall(url, method, data, null);
}
+ @SuppressWarnings("CharsetObjectCanBeUsed")
private static String performCall(final URL url, final String method, final String data, final HttpURLConnection existingConnection) {
try {
final HttpURLConnection connection = existingConnection != null
@@ -160,7 +162,7 @@ public class NetworkUtils {
if (data != null && !data.isEmpty()) {
connection.setDoOutput(true);
final OutputStream output = connection.getOutputStream();
- output.write(data.getBytes(Charset.forName(UTF8)));
+ output.write(data.getBytes(Charset.forName("UTF-8")));
output.flush();
output.close();
}
diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java
index 3d49b083..9f5b0147 100644
--- a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java
+++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java
@@ -78,7 +78,7 @@ import static android.app.Activity.RESULT_OK;
* Also allows to parse/fetch information out of shared information.
* (M)Permissions are not checked, wrap ShareUtils methods if neccessary
*/
-@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection", "JavadocReference"})
+@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection", "JavadocReference", "ConstantLocale"})
public class ShareUtil {
public final static String EXTRA_FILEPATH = "real_file_path_2";
public final static SimpleDateFormat SDF_RFC3339_ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.getDefault());
@@ -90,6 +90,8 @@ public class ShareUtil {
public final static int REQUEST_PICK_PICTURE = 50002;
public final static int REQUEST_SAF = 50003;
+ public final static int MIN_OVERWRITE_LENGTH = 5;
+
protected static String _lastCameraPictureFilepath;
protected Context _context;
@@ -1132,7 +1134,9 @@ public class ShareUtil {
try {
FileOutputStream fileOutputStream = null;
ParcelFileDescriptor pfd = null;
- if (file.canWrite() || (!file.exists() && file.getParentFile().canWrite())) {
+ final boolean existingEmptyFile = file.canWrite() && file.length() < MIN_OVERWRITE_LENGTH;
+ final boolean nonExistingCreatableFile = !file.exists() && file.getParentFile().canWrite();
+ if (existingEmptyFile || nonExistingCreatableFile) {
if (isDirectory) {
file.mkdirs();
} else {
@@ -1144,7 +1148,7 @@ public class ShareUtil {
if (isDirectory) {
// Nothing to do
} else {
- pfd = _context.getContentResolver().openFileDescriptor(dof.getUri(), "w");
+ pfd = _context.getContentResolver().openFileDescriptor(dof.getUri(), "rw");
fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
}
}
diff --git a/build.gradle b/build.gradle
index 69f42bd6..0ecd8134 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,8 +13,8 @@ import java.text.SimpleDateFormat
buildscript {
ext {
- version_gradle_tools = "3.6.1"
- version_plugin_kotlin = "1.3.71"
+ version_gradle_tools = "3.6.3"
+ version_plugin_kotlin = "1.3.72"
enable_plugin_kotlin = false
version_compileSdk = 28
@@ -59,6 +59,13 @@ allprojects {
tasks.matching { task -> task.name.matches('.*generate.*Resources') }.all {
task -> task.dependsOn copyRepoFiles
}
+
+ tasks.matching {it instanceof Test}.all { // Enable unit test output, html+xml output
+ testLogging.events "passed", "skipped", "failed", "standardOut", "standardError"
+ testLogging.showStandardStreams = true
+ reports.junitXml.enabled = true
+ reports.html.enabled = true
+ }
}
task clean(type: Delete) {