Render Markdown natively
Stop using problematic WebView
This commit is contained in:
parent
dd2c9eeafe
commit
aad9aced18
@ -80,8 +80,9 @@ dependencies {
|
|||||||
fullImplementation "androidx.cardview:cardview:${rootProject.ext.androidXVersion}"
|
fullImplementation "androidx.cardview:cardview:${rootProject.ext.androidXVersion}"
|
||||||
fullImplementation "com.google.android.material:material:${rootProject.ext.androidXVersion}"
|
fullImplementation "com.google.android.material:material:${rootProject.ext.androidXVersion}"
|
||||||
fullImplementation 'com.github.topjohnwu:libsu:2.1.2'
|
fullImplementation 'com.github.topjohnwu:libsu:2.1.2'
|
||||||
fullImplementation 'com.atlassian.commonmark:commonmark:0.11.0'
|
|
||||||
fullImplementation 'org.kamranzafar:jtar:2.3'
|
fullImplementation 'org.kamranzafar:jtar:2.3'
|
||||||
|
fullImplementation 'ru.noties:markwon:2.0.1'
|
||||||
|
fullImplementation 'com.caverock:androidsvg-aar:1.3'
|
||||||
|
|
||||||
def butterKnifeVersion = '9.0.0-rc2'
|
def butterKnifeVersion = '9.0.0-rc2'
|
||||||
if (properties.containsKey('android.injected.invoked.from.ide')) {
|
if (properties.containsKey('android.injected.invoked.from.ide')) {
|
||||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -26,6 +26,9 @@
|
|||||||
-keepclassmembers class com.topjohnwu.core.utils.ISafetyNetHelper { *; }
|
-keepclassmembers class com.topjohnwu.core.utils.ISafetyNetHelper { *; }
|
||||||
-keepclassmembers class com.topjohnwu.core.utils.BootSigner { *; }
|
-keepclassmembers class com.topjohnwu.core.utils.BootSigner { *; }
|
||||||
|
|
||||||
|
# SVG
|
||||||
|
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
|
||||||
|
|
||||||
# Strip logging
|
# Strip logging
|
||||||
-assumenosideeffects class com.topjohnwu.core.utils.Logger {
|
-assumenosideeffects class com.topjohnwu.core.utils.Logger {
|
||||||
public *** debug(...);
|
public *** debug(...);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name="a.q"
|
android:name="a.q"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<!-- Activities -->
|
<!-- Activities -->
|
||||||
|
@ -52,8 +52,8 @@ public class AboutActivity extends BaseActivity {
|
|||||||
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getPackageName()));
|
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getPackageName()));
|
||||||
|
|
||||||
appChangelog.setOnClickListener(v -> {
|
appChangelog.setOnClickListener(v -> {
|
||||||
new MarkDownWindow(this, getString(R.string.app_changelog),
|
MarkDownWindow.show(this, getString(R.string.app_changelog),
|
||||||
getResources().openRawResource(R.raw.changelog)).exec();
|
getResources().openRawResource(R.raw.changelog));
|
||||||
});
|
});
|
||||||
|
|
||||||
String translators = getString(R.string.translators);
|
String translators = getString(R.string.translators);
|
||||||
|
@ -101,7 +101,7 @@ public class ReposAdapter extends SectionedAdapter<ReposAdapter.SectionHolder, R
|
|||||||
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
|
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
|
||||||
|
|
||||||
holder.infoLayout.setOnClickListener(v ->
|
holder.infoLayout.setOnClickListener(v ->
|
||||||
new MarkDownWindow((BaseActivity) context, null, repo.getDetailUrl()).exec());
|
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
|
||||||
|
|
||||||
holder.downloadImage.setOnClickListener(v -> {
|
holder.downloadImage.setOnClickListener(v -> {
|
||||||
new CustomAlertDialog((BaseActivity) context)
|
new CustomAlertDialog((BaseActivity) context)
|
||||||
|
@ -41,7 +41,7 @@ public class MagiskInstallDialog extends CustomAlertDialog {
|
|||||||
// Open forum links in browser
|
// Open forum links in browser
|
||||||
AppUtils.openLink(a, Uri.parse(Data.magiskNoteLink));
|
AppUtils.openLink(a, Uri.parse(Data.magiskNoteLink));
|
||||||
} else {
|
} else {
|
||||||
new MarkDownWindow(a, null, Data.magiskNoteLink).exec();
|
MarkDownWindow.show(a, null, Data.magiskNoteLink);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,7 @@ public class ManagerInstallDialog extends CustomAlertDialog {
|
|||||||
setPositiveButton(R.string.install, (d, i) -> DownloadApp.upgrade(name));
|
setPositiveButton(R.string.install, (d, i) -> DownloadApp.upgrade(name));
|
||||||
setNegativeButton(R.string.no_thanks, null);
|
setNegativeButton(R.string.no_thanks, null);
|
||||||
if (!TextUtils.isEmpty(Data.managerNoteLink)) {
|
if (!TextUtils.isEmpty(Data.managerNoteLink)) {
|
||||||
setNeutralButton(R.string.app_changelog, (d, i) ->
|
setNeutralButton(R.string.app_changelog, (d, i) -> MarkDownWindow.show(a, null, Data.managerNoteLink));
|
||||||
new MarkDownWindow(a, null, Data.managerNoteLink).exec());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,86 +1,107 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.webkit.WebView;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.caverock.androidsvg.SVG;
|
||||||
|
import com.caverock.androidsvg.SVGParseException;
|
||||||
import com.topjohnwu.core.App;
|
import com.topjohnwu.core.App;
|
||||||
import com.topjohnwu.core.Data;
|
|
||||||
import com.topjohnwu.core.tasks.ParallelTask;
|
|
||||||
import com.topjohnwu.core.utils.Utils;
|
import com.topjohnwu.core.utils.Utils;
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.net.Networking;
|
||||||
|
import com.topjohnwu.net.ResponseListener;
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
import com.topjohnwu.superuser.ShellUtils;
|
||||||
|
import com.topjohnwu.utils.ByteArrayStream;
|
||||||
import org.commonmark.node.Node;
|
|
||||||
import org.commonmark.parser.Parser;
|
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import ru.noties.markwon.Markwon;
|
||||||
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
public class MarkDownWindow extends ParallelTask<Void, Void, String> {
|
public class MarkDownWindow {
|
||||||
|
|
||||||
private String mTitle;
|
private static final SpannableConfiguration config = SpannableConfiguration.builder(App.self)
|
||||||
private String mUrl;
|
.asyncDrawableLoader(new Loader()).build();
|
||||||
private InputStream is;
|
|
||||||
|
|
||||||
|
public static void show(Activity activity, String title, String url) {
|
||||||
public MarkDownWindow(Activity context, String title, String url) {
|
Networking.get(url).getAsString(new Listener(activity, title));
|
||||||
super(context);
|
|
||||||
mTitle = title;
|
|
||||||
mUrl = url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarkDownWindow(Activity context, String title, InputStream in) {
|
public static void show (Activity activity, String title, InputStream is) {
|
||||||
super(context);
|
try {
|
||||||
mTitle = title;
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
is = in;
|
ShellUtils.pump(is, baos);
|
||||||
|
new Listener(activity, title).onResponse(baos.toString());
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Listener implements ResponseListener<String> {
|
||||||
|
|
||||||
|
Activity activity;
|
||||||
|
String title;
|
||||||
|
|
||||||
|
Listener(Activity a, String t) {
|
||||||
|
activity = a;
|
||||||
|
title = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(Void... voids) {
|
public void onResponse(String md) {
|
||||||
App app = App.self;
|
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||||
String md;
|
alert.setTitle(title);
|
||||||
if (mUrl != null) {
|
View mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null);
|
||||||
md = Utils.dlString(mUrl);
|
Markwon.setMarkdown(mv.findViewById(R.id.md_txt), config, md);
|
||||||
} else {
|
alert.setView(mv);
|
||||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
ShellUtils.pump(is, out);
|
|
||||||
md = out.toString();
|
|
||||||
is.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String css;
|
|
||||||
try (InputStream in = app.getResources()
|
|
||||||
.openRawResource(Data.isDarkTheme ? R.raw.dark : R.raw.light);
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
css = out.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
Parser parser = Parser.builder().build();
|
|
||||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
|
||||||
Node doc = parser.parse(md);
|
|
||||||
return String.format("<style>%s</style>%s", css, renderer.render(doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String html) {
|
|
||||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
|
||||||
alert.setTitle(mTitle);
|
|
||||||
|
|
||||||
WebView wv = new WebView(getActivity());
|
|
||||||
wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null);
|
|
||||||
|
|
||||||
alert.setView(wv);
|
|
||||||
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
|
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
|
||||||
alert.show();
|
alert.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class Loader implements AsyncDrawable.Loader {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load(@NonNull String url, @NonNull AsyncDrawable asyncDrawable) {
|
||||||
|
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||||
|
InputStream is = Networking.get(url).execForInputStream().getResult();
|
||||||
|
if (is == null)
|
||||||
|
return;
|
||||||
|
ByteArrayStream buf = new ByteArrayStream();
|
||||||
|
buf.readFrom(is);
|
||||||
|
// First try default drawables
|
||||||
|
Drawable drawable = Drawable.createFromStream(buf.getInputStream(), "");
|
||||||
|
if (drawable == null) {
|
||||||
|
// SVG
|
||||||
|
try {
|
||||||
|
SVG svg = SVG.getFromInputStream(buf.getInputStream());
|
||||||
|
int width = Utils.dpInPx((int) svg.getDocumentWidth());
|
||||||
|
int height = Utils.dpInPx((int) svg.getDocumentHeight());
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
float density = App.self.getResources().getDisplayMetrics().density;
|
||||||
|
canvas.scale(density, density);
|
||||||
|
svg.renderToCanvas(canvas);
|
||||||
|
drawable = new BitmapDrawable(App.self.getResources(), bitmap);
|
||||||
|
} catch (SVGParseException ignored) {}
|
||||||
|
}
|
||||||
|
if (drawable != null) {
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
asyncDrawable.setResult(drawable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(@NonNull String url) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
14
app/src/full/res/layout/markdown_window.xml
Normal file
14
app/src/full/res/layout/markdown_window.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/md_txt"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="15dp"
|
||||||
|
android:layout_marginEnd="15dp"
|
||||||
|
android:paddingTop="10dp" />
|
||||||
|
|
||||||
|
</ScrollView>
|
@ -1,26 +0,0 @@
|
|||||||
package com.topjohnwu.core.tasks;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public abstract class ParallelTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
|
||||||
|
|
||||||
private WeakReference<Activity> weakActivity;
|
|
||||||
|
|
||||||
public ParallelTask() {}
|
|
||||||
|
|
||||||
public ParallelTask(Activity context) {
|
|
||||||
weakActivity = new WeakReference<>(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Activity getActivity() {
|
|
||||||
return weakActivity.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void exec(Params... params) {
|
|
||||||
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user