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 "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')) {

View File

@ -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(...);

View File

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

View File

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

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.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)

View File

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

View File

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

View File

@ -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) {}
} }
@Override static class Listener implements ResponseListener<String> {
protected String doInBackground(Void... voids) {
App app = App.self; Activity activity;
String md; String title;
if (mUrl != null) {
md = Utils.dlString(mUrl); Listener(Activity a, String t) {
} else { activity = a;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { title = t;
ShellUtils.pump(is, out);
md = out.toString();
is.close();
} catch (IOException e) {
e.printStackTrace();
return "";
}
} }
String css;
try (InputStream in = app.getResources() @Override
.openRawResource(Data.isDarkTheme ? R.raw.dark : R.raw.light); public void onResponse(String md) {
ByteArrayOutputStream out = new ByteArrayOutputStream()) { AlertDialog.Builder alert = new AlertDialog.Builder(activity);
ShellUtils.pump(in, out); alert.setTitle(title);
css = out.toString(); View mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null);
} catch (IOException e) { Markwon.setMarkdown(mv.findViewById(R.id.md_txt), config, md);
e.printStackTrace(); alert.setView(mv);
return ""; 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 static class Loader implements AsyncDrawable.Loader {
protected void onPostExecute(String html) {
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setTitle(mTitle);
WebView wv = new WebView(getActivity()); @Override
wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null); 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); @Override
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss()); public void cancel(@NonNull String url) {}
alert.show();
} }
} }

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