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 data = new ArrayList<>(); - public List highlightData = new ArrayList<>(); - public List iconsForData = new ArrayList<>(); + public Callback.a2 withPositionCallback; + public List data; + public List 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) {