Render Markdown natively

Stop using problematic WebView
This commit is contained in:
topjohnwu 2018-12-23 19:29:25 +08:00
parent dd2c9eeafe
commit aad9aced18
10 changed files with 103 additions and 90 deletions

View File

@ -80,8 +80,9 @@ dependencies {
fullImplementation "androidx.cardview:cardview:${rootProject.ext.androidXVersion}"
fullImplementation "com.google.android.material:material:${rootProject.ext.androidXVersion}"
fullImplementation 'com.github.topjohnwu:libsu:2.1.2'
fullImplementation 'com.atlassian.commonmark:commonmark:0.11.0'
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'
if (properties.containsKey('android.injected.invoked.from.ide')) {

View File

@ -26,6 +26,9 @@
-keepclassmembers class com.topjohnwu.core.utils.ISafetyNetHelper { *; }
-keepclassmembers class com.topjohnwu.core.utils.BootSigner { *; }
# SVG
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
# Strip logging
-assumenosideeffects class com.topjohnwu.core.utils.Logger {
public *** debug(...);

View File

@ -11,6 +11,7 @@
<application
android:name="a.q"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
<!-- Activities -->

View File

@ -52,8 +52,8 @@ public class AboutActivity extends BaseActivity {
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getPackageName()));
appChangelog.setOnClickListener(v -> {
new MarkDownWindow(this, getString(R.string.app_changelog),
getResources().openRawResource(R.raw.changelog)).exec();
MarkDownWindow.show(this, getString(R.string.app_changelog),
getResources().openRawResource(R.raw.changelog));
});
String translators = getString(R.string.translators);

View File

@ -101,7 +101,7 @@ public class ReposAdapter extends SectionedAdapter<ReposAdapter.SectionHolder, R
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
holder.infoLayout.setOnClickListener(v ->
new MarkDownWindow((BaseActivity) context, null, repo.getDetailUrl()).exec());
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
holder.downloadImage.setOnClickListener(v -> {
new CustomAlertDialog((BaseActivity) context)

View File

@ -41,7 +41,7 @@ public class MagiskInstallDialog extends CustomAlertDialog {
// Open forum links in browser
AppUtils.openLink(a, Uri.parse(Data.magiskNoteLink));
} else {
new MarkDownWindow(a, null, Data.magiskNoteLink).exec();
MarkDownWindow.show(a, null, Data.magiskNoteLink);
}
});
}

View File

@ -21,8 +21,7 @@ public class ManagerInstallDialog extends CustomAlertDialog {
setPositiveButton(R.string.install, (d, i) -> DownloadApp.upgrade(name));
setNegativeButton(R.string.no_thanks, null);
if (!TextUtils.isEmpty(Data.managerNoteLink)) {
setNeutralButton(R.string.app_changelog, (d, i) ->
new MarkDownWindow(a, null, Data.managerNoteLink).exec());
setNeutralButton(R.string.app_changelog, (d, i) -> MarkDownWindow.show(a, null, Data.managerNoteLink));
}
}
}

View File

@ -1,86 +1,107 @@
package com.topjohnwu.magisk.components;
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.Data;
import com.topjohnwu.core.tasks.ParallelTask;
import com.topjohnwu.core.utils.Utils;
import com.topjohnwu.magisk.R;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.ResponseListener;
import com.topjohnwu.superuser.ShellUtils;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import com.topjohnwu.utils.ByteArrayStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import androidx.annotation.NonNull;
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 String mUrl;
private InputStream is;
private static final SpannableConfiguration config = SpannableConfiguration.builder(App.self)
.asyncDrawableLoader(new Loader()).build();
public MarkDownWindow(Activity context, String title, String url) {
super(context);
mTitle = title;
mUrl = url;
public static void show(Activity activity, String title, String url) {
Networking.get(url).getAsString(new Listener(activity, title));
}
public MarkDownWindow(Activity context, String title, InputStream in) {
super(context);
mTitle = title;
is = in;
public static void show (Activity activity, String title, InputStream is) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ShellUtils.pump(is, baos);
new Listener(activity, title).onResponse(baos.toString());
} catch (IOException ignored) {}
}
@Override
protected String doInBackground(Void... voids) {
App app = App.self;
String md;
if (mUrl != null) {
md = Utils.dlString(mUrl);
} else {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ShellUtils.pump(is, out);
md = out.toString();
is.close();
} catch (IOException e) {
e.printStackTrace();
return "";
}
static class Listener implements ResponseListener<String> {
Activity activity;
String title;
Listener(Activity a, String t) {
activity = a;
title = t;
}
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 "";
@Override
public void onResponse(String md) {
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
alert.setTitle(title);
View mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null);
Markwon.setMarkdown(mv.findViewById(R.id.md_txt), config, md);
alert.setView(mv);
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
alert.show();
}
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);
static class Loader implements AsyncDrawable.Loader {
WebView wv = new WebView(getActivity());
wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null);
@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);
}
});
}
alert.setView(wv);
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
alert.show();
@Override
public void cancel(@NonNull String url) {}
}
}

View 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>

View File

@ -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);
}
}