mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-11-24 02:25:28 +00:00
parent
d8b1d79879
commit
ac2a9da4c4
@ -0,0 +1,244 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.PictureDrawable
|
||||||
|
import android.graphics.drawable.ShapeDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.DynamicDrawableSpan
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.view.marginLeft
|
||||||
|
import androidx.core.view.marginRight
|
||||||
|
import com.caverock.androidsvg.SVG
|
||||||
|
import com.caverock.androidsvg.SVGParseException
|
||||||
|
import com.topjohnwu.magisk.core.ResMgr
|
||||||
|
import com.topjohnwu.superuser.internal.WaitRunnable
|
||||||
|
import io.noties.markwon.*
|
||||||
|
import io.noties.markwon.image.*
|
||||||
|
import io.noties.markwon.image.data.DataUriSchemeHandler
|
||||||
|
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.commonmark.node.Image
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
// Differences with Markwon stock ImagePlugin:
|
||||||
|
//
|
||||||
|
// We assume beforeSetText() will be run in a background thread, and in that method
|
||||||
|
// we download/decode all drawables before sending the spanned markdown CharSequence
|
||||||
|
// to the next stage. We also get our surrounding TextView width to properly
|
||||||
|
// resize our images.
|
||||||
|
//
|
||||||
|
// This is required for PrecomputedText to properly take the images into account
|
||||||
|
// when precomputing the metrics of TextView
|
||||||
|
//
|
||||||
|
// Basically, we want nothing to do with AsyncDrawable
|
||||||
|
class MarkwonImagePlugin(okHttp: OkHttpClient) : AbstractMarkwonPlugin() {
|
||||||
|
|
||||||
|
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
||||||
|
builder.setFactory(Image::class.java, ImageSpanFactory())
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun beforeSetText(tv: TextView, markdown: Spanned) {
|
||||||
|
if (markdown.isEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
val spans = markdown.getSpans(0, markdown.length, ImageSpan::class.java)
|
||||||
|
if (spans == null || spans.isEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
// Download and create all drawables in parallel
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
// Download and decode all drawables in parallel
|
||||||
|
val defers = spans.map { async { it.load() } }
|
||||||
|
|
||||||
|
// Get TextView sizes beforehand to resize all images
|
||||||
|
// We get its parent here as the TextView itself might be hidden
|
||||||
|
val parent = tv.parent as View
|
||||||
|
parent.post(WaitRunnable{
|
||||||
|
// Make sure it is rendered
|
||||||
|
val width = parent.width -
|
||||||
|
tv.paddingLeft - tv.paddingRight -
|
||||||
|
tv.marginLeft - tv.marginRight
|
||||||
|
spans.forEach { it.canvasWidth = width }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure all is done before returning
|
||||||
|
defers.awaitAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val schemeHandlers = HashMap<String, SchemeHandler>(3)
|
||||||
|
private val mediaDecoders = HashMap<String, MediaDecoder>(0)
|
||||||
|
private val defaultMediaDecoder = DefaultMediaDecoder.create()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addSchemeHandler(DataUriSchemeHandler.create())
|
||||||
|
addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttp))
|
||||||
|
addMediaDecoder(SVGDecoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSchemeHandler(schemeHandler: SchemeHandler) {
|
||||||
|
for (scheme in schemeHandler.supportedSchemes()) {
|
||||||
|
schemeHandlers[scheme] = schemeHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addMediaDecoder(mediaDecoder: MediaDecoder) {
|
||||||
|
for (type in mediaDecoder.supportedTypes()) {
|
||||||
|
mediaDecoders[type] = mediaDecoder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified from AsyncDrawableLoaderImpl.execute(asyncDrawable)
|
||||||
|
fun loadDrawable(destination: String): Drawable? {
|
||||||
|
val uri = Uri.parse(destination)
|
||||||
|
var drawable: Drawable? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val scheme = uri.scheme
|
||||||
|
check(scheme != null && scheme.isNotEmpty()) {
|
||||||
|
"No scheme is found: $destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtain scheme handler
|
||||||
|
val schemeHandler = schemeHandlers[scheme]
|
||||||
|
?: throw IllegalStateException("No scheme-handler is found: $destination")
|
||||||
|
|
||||||
|
// handle scheme
|
||||||
|
val imageItem = schemeHandler.handle(destination, uri)
|
||||||
|
|
||||||
|
// if resulting imageItem needs further decoding -> proceed
|
||||||
|
drawable = if (imageItem.hasDecodingNeeded()) {
|
||||||
|
val withDecodingNeeded = imageItem.asWithDecodingNeeded
|
||||||
|
val mediaDecoder = mediaDecoders[withDecodingNeeded.contentType()]
|
||||||
|
?: defaultMediaDecoder
|
||||||
|
mediaDecoder.decode(
|
||||||
|
withDecodingNeeded.contentType(),
|
||||||
|
withDecodingNeeded.inputStream()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
imageItem.asWithResult.result()
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "Error loading image: $destination")
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply intrinsic bounds (but only if they are empty)
|
||||||
|
if (drawable != null && drawable.bounds.isEmpty)
|
||||||
|
DrawableUtils.applyIntrinsicBounds(drawable)
|
||||||
|
|
||||||
|
return drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ImageSpanFactory : SpanFactory {
|
||||||
|
override fun getSpans(configuration: MarkwonConfiguration, props: RenderProps): Any? {
|
||||||
|
val dest = ImageProps.DESTINATION.require(props)
|
||||||
|
val size = ImageProps.IMAGE_SIZE.get(props)
|
||||||
|
return ImageSpan(dest, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ImageSpan(
|
||||||
|
private val dest: String,
|
||||||
|
private val size: ImageSize?
|
||||||
|
) : DynamicDrawableSpan(ALIGN_BOTTOM) {
|
||||||
|
|
||||||
|
var canvasWidth = 0
|
||||||
|
var measured = false
|
||||||
|
lateinit var draw: Drawable
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
draw = loadDrawable(dest) ?: ShapeDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDrawable() = draw
|
||||||
|
|
||||||
|
private fun defaultBounds(): Rect {
|
||||||
|
val bounds: Rect = draw.bounds
|
||||||
|
if (!bounds.isEmpty) {
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
val intrinsicBounds = DrawableUtils.intrinsicBounds(draw)
|
||||||
|
if (!intrinsicBounds.isEmpty) {
|
||||||
|
return intrinsicBounds
|
||||||
|
}
|
||||||
|
return Rect(0, 0, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun measure(paint: Paint) {
|
||||||
|
if (measured)
|
||||||
|
return
|
||||||
|
if (canvasWidth == 0)
|
||||||
|
return
|
||||||
|
measured = true
|
||||||
|
val bound = SizeResolver.resolveImageSize(size, defaultBounds(), canvasWidth, paint.textSize)
|
||||||
|
draw.bounds = bound
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
measure(paint)
|
||||||
|
return super.getSize(paint, text, start, end, fm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SizeResolver : ImageSizeResolverDef() {
|
||||||
|
// Expose protected API
|
||||||
|
public override fun resolveImageSize(
|
||||||
|
imageSize: ImageSize?,
|
||||||
|
imageBounds: Rect,
|
||||||
|
canvasWidth: Int,
|
||||||
|
textSize: Float
|
||||||
|
): Rect {
|
||||||
|
return super.resolveImageSize(imageSize, imageBounds, canvasWidth, textSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SVGDecoder: MediaDecoder() {
|
||||||
|
override fun supportedTypes() = listOf("image/svg+xml")
|
||||||
|
|
||||||
|
override fun decode(contentType: String?, inputStream: InputStream): Drawable {
|
||||||
|
val svg: SVG
|
||||||
|
svg = try {
|
||||||
|
SVG.getFromInputStream(inputStream)
|
||||||
|
} catch (e: SVGParseException) {
|
||||||
|
throw IllegalStateException("Exception decoding SVG", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val w = svg.documentWidth
|
||||||
|
val h = svg.documentHeight
|
||||||
|
|
||||||
|
if (w == 0f || h == 0f) {
|
||||||
|
val picture = svg.renderToPicture()
|
||||||
|
return PictureDrawable(picture)
|
||||||
|
}
|
||||||
|
|
||||||
|
val density: Float = ResMgr.resource.displayMetrics.density
|
||||||
|
|
||||||
|
val width = (w * density + .5f).toInt()
|
||||||
|
val height = (h * density + .5f).toInt()
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
canvas.scale(density, density)
|
||||||
|
svg.renderToCanvas(canvas)
|
||||||
|
|
||||||
|
return BitmapDrawable(ResMgr.resource, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@ package com.topjohnwu.magisk.core.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -24,11 +23,6 @@ object Utils {
|
|||||||
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
|
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dpInPx(dp: Int): Int {
|
|
||||||
val scale = get<Resources>().displayMetrics.density
|
|
||||||
return (dp * scale + 0.5).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSuperUser(): Boolean {
|
fun showSuperUser(): Boolean {
|
||||||
return Info.env.isActive && (Const.USER_ID == 0
|
return Info.env.isActive && (Const.USER_ID == 0
|
||||||
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
||||||
|
@ -2,15 +2,13 @@ package com.topjohnwu.magisk.databinding
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.text.PrecomputedTextCompat
|
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
|
import com.topjohnwu.magisk.ktx.coroutineScope
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@BindingAdapter("gone")
|
@BindingAdapter("gone")
|
||||||
@ -33,18 +31,10 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
|||||||
setInvisible(view, invisibleUnless.not())
|
setInvisible(view, invisibleUnless.not())
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("precomputedText")
|
|
||||||
fun setPrecomputedText(tv: TextView, text: CharSequence) {
|
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
|
||||||
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
|
||||||
tv.post {
|
|
||||||
TextViewCompat.setPrecomputedText(tv, pre);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindingAdapter("markdownText")
|
@BindingAdapter("markdownText")
|
||||||
fun setMarkdownText(tv: TextView, text: CharSequence) {
|
fun setMarkdownText(tv: TextView, text: CharSequence) {
|
||||||
|
tv.coroutineScope.launch(Dispatchers.IO) {
|
||||||
val markwon = get<Markwon>()
|
val markwon = get<Markwon>()
|
||||||
markwon.setMarkdown(tv, text.toString())
|
markwon.setMarkdown(tv, text.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,14 @@ import android.os.Build
|
|||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.utils.MarkwonImagePlugin
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
|
import com.topjohnwu.magisk.ktx.precomputedText
|
||||||
import com.topjohnwu.magisk.net.Networking
|
import com.topjohnwu.magisk.net.Networking
|
||||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||||
import com.topjohnwu.magisk.view.PrecomputedTextSetter
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.html.HtmlPlugin
|
import io.noties.markwon.html.HtmlPlugin
|
||||||
import io.noties.markwon.image.ImagesPlugin
|
|
||||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -96,10 +95,11 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
|||||||
|
|
||||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||||
return Markwon.builder(context)
|
return Markwon.builder(context)
|
||||||
.textSetter(PrecomputedTextSetter())
|
.textSetter { textView, spanned, _, onComplete ->
|
||||||
|
textView.tag = onComplete
|
||||||
|
textView.precomputedText = spanned
|
||||||
|
}
|
||||||
.usePlugin(HtmlPlugin.create())
|
.usePlugin(HtmlPlugin.create())
|
||||||
.usePlugin(ImagesPlugin.create {
|
.usePlugin(MarkwonImagePlugin(okHttpClient))
|
||||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
|
||||||
})
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,12 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import android.text.PrecomputedText
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
@ -31,13 +35,26 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.text.PrecomputedTextCompat
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
import com.topjohnwu.magisk.FileProvider
|
import com.topjohnwu.magisk.FileProvider
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.ResMgr
|
||||||
import com.topjohnwu.magisk.core.utils.Utils
|
import com.topjohnwu.magisk.core.utils.Utils
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.lang.reflect.Array as JArray
|
import java.lang.reflect.Array as JArray
|
||||||
@ -328,3 +345,78 @@ fun Activity.hideKeyboard() {
|
|||||||
fun Fragment.hideKeyboard() {
|
fun Fragment.hideKeyboard() {
|
||||||
activity?.hideKeyboard()
|
activity?.hideKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
||||||
|
|
||||||
|
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
||||||
|
viewTreeObserver.addOnGlobalLayoutListener(object :
|
||||||
|
ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun ViewGroup.startAnimations() {
|
||||||
|
val transition = AutoTransition()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator()).setDuration(400)
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
this,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var View.coroutineScope: CoroutineScope
|
||||||
|
get() = getTag(R.id.coroutineScope) as? CoroutineScope ?: GlobalScope
|
||||||
|
set(value) = setTag(R.id.coroutineScope, value)
|
||||||
|
|
||||||
|
@set:BindingAdapter("precomputedText")
|
||||||
|
var TextView.precomputedText: CharSequence
|
||||||
|
get() = text
|
||||||
|
set(value) {
|
||||||
|
val callback = tag as? Runnable
|
||||||
|
|
||||||
|
// Don't even bother pre 21
|
||||||
|
if (SDK_INT < 21) {
|
||||||
|
post {
|
||||||
|
text = value
|
||||||
|
isGone = false
|
||||||
|
callback?.run()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
if (SDK_INT >= 29) {
|
||||||
|
// Internally PrecomputedTextCompat will platform API on API 29+
|
||||||
|
// Due to some stupid crap OEM (Samsung) implementation, this can actually
|
||||||
|
// crash our app. Directly use platform APIs with some workarounds
|
||||||
|
val pre = PrecomputedText.create(value, textMetricsParams)
|
||||||
|
post {
|
||||||
|
try {
|
||||||
|
text = pre
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// Override to computed params to workaround crashes
|
||||||
|
textMetricsParams = pre.params
|
||||||
|
text = pre
|
||||||
|
}
|
||||||
|
isGone = false
|
||||||
|
callback?.run()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val tv = this@precomputedText
|
||||||
|
val params = TextViewCompat.getTextMetricsParams(tv)
|
||||||
|
val pre = PrecomputedTextCompat.create(value, params)
|
||||||
|
post {
|
||||||
|
TextViewCompat.setPrecomputedText(tv, pre)
|
||||||
|
isGone = false
|
||||||
|
callback?.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.dpInPx(): Int {
|
||||||
|
val scale = ResMgr.resource.displayMetrics.density
|
||||||
|
return (this * scale + 0.5).toInt()
|
||||||
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewTreeObserver
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import androidx.transition.AutoTransition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
|
|
||||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
|
||||||
|
|
||||||
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
|
||||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
|
||||||
override fun onGlobalLayout() {
|
|
||||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fun ViewGroup.startAnimations() {
|
|
||||||
val transition = AutoTransition().setInterpolator(FastOutSlowInInterpolator()).setDuration(400)
|
|
||||||
TransitionManager.beginDelayedTransition(this, transition)
|
|
||||||
}
|
|
@ -1,34 +1,21 @@
|
|||||||
package com.topjohnwu.magisk.view
|
package com.topjohnwu.magisk.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Spanned
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.text.PrecomputedTextCompat
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||||
|
import com.topjohnwu.magisk.ktx.coroutineScope
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
class PrecomputedTextSetter : Markwon.TextSetter {
|
|
||||||
|
|
||||||
override fun setText(tv: TextView, text: Spanned, bufferType: TextView.BufferType, onComplete: Runnable) {
|
|
||||||
val scope = tv.tag as? CoroutineScope ?: GlobalScope
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
|
||||||
tv.post {
|
|
||||||
TextViewCompat.setPrecomputedText(tv, pre)
|
|
||||||
onComplete.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object MarkDownWindow : KoinComponent {
|
object MarkDownWindow : KoinComponent {
|
||||||
|
|
||||||
private val repo: StringRepository by inject()
|
private val repo: StringRepository by inject()
|
||||||
@ -41,11 +28,19 @@ object MarkDownWindow : KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun show(activity: Context, title: String?, input: suspend () -> String) {
|
suspend fun show(activity: Context, title: String?, input: suspend () -> String) {
|
||||||
val mdRes = R.layout.markdown_window_md2
|
val view = LayoutInflater.from(activity).inflate(R.layout.markdown_window_md2, null)
|
||||||
val mv = LayoutInflater.from(activity).inflate(mdRes, null)
|
|
||||||
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
|
||||||
tv.tag = CoroutineScope(coroutineContext)
|
|
||||||
|
|
||||||
|
MagiskDialog(activity)
|
||||||
|
.applyTitle(title ?: "")
|
||||||
|
.applyView(view)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
titleRes = android.R.string.cancel
|
||||||
|
}
|
||||||
|
.reveal()
|
||||||
|
|
||||||
|
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||||
|
tv.coroutineScope = CoroutineScope(coroutineContext)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
markwon.setMarkdown(tv, input())
|
markwon.setMarkdown(tv, input())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -54,13 +49,6 @@ object MarkDownWindow : KoinComponent {
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
tv.setText(R.string.download_file_error)
|
tv.setText(R.string.download_file_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
MagiskDialog(activity)
|
|
||||||
.applyTitle(title ?: "")
|
|
||||||
.applyView(mv)
|
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
titleRes = android.R.string.cancel
|
|
||||||
}
|
}
|
||||||
.reveal()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,6 +233,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="15dp"
|
android:layout_margin="15dp"
|
||||||
android:textAppearance="@style/AppearanceFoundation.Caption"
|
android:textAppearance="@style/AppearanceFoundation.Caption"
|
||||||
|
android:visibility="gone"
|
||||||
markdownText="@{viewModel.notes}"/>
|
markdownText="@{viewModel.notes}"/>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<item name="recyclerScrollListener" type="id" />
|
<item name="recyclerScrollListener" type="id" />
|
||||||
|
<item name="coroutineScope" type="id"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user