Module json add changelog

This commit is contained in:
vvb2060
2022-01-21 23:52:42 +08:00
committed by John Wu
parent 691e41e22e
commit bf8b74e996
62 changed files with 183 additions and 447 deletions

View File

@@ -14,20 +14,19 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.synchronized
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Notifications.mgr
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
import java.util.*
import kotlin.collections.HashMap
class DownloadService : BaseService() {
@@ -67,11 +66,11 @@ class DownloadService : BaseService() {
val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) {
is Subject.Manager -> handleAPK(subject, stream)
else -> stream.copyAndClose(subject.file.outputStream())
else -> stream.toModule(subject.file, service.fetchInstaller().byteStream())
}
if (ForegroundTracker.hasForeground) {
remove(subject.notifyId)
subject.pendingIntent(this@DownloadService).send()
subject.pendingIntent(this@DownloadService)?.send()
} else {
notifyFinish(subject)
}

View File

@@ -1,14 +1,18 @@
package com.topjohnwu.magisk.core.download
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Intent
import androidx.core.content.getSystemService
import androidx.core.net.toFile
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.writeTo
import java.io.File
import java.io.InputStream
@@ -55,9 +59,17 @@ suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStr
apk.delete()
patched.renameTo(apk)
} else {
// Simply relaunch the app
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//noinspection InlinedApi
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val pending = PendingIntent.getActivity(this, id, intent, flag)
if (ForegroundTracker.hasForeground) {
val alarm = getSystemService<AlarmManager>()
alarm!!.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pending)
}
stopSelf()
relaunchApp(this)
Runtime.getRuntime().exit(0)
}
} else {
write(subject.file.outputStream())

View File

@@ -0,0 +1,38 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.forEach
import com.topjohnwu.magisk.ktx.withStreams
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
fun InputStream.toModule(file: Uri, installer: InputStream) {
val input = ZipInputStream(buffered())
val output = ZipOutputStream(file.outputStream().buffered())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
installer.copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
}
}
}

View File

@@ -35,7 +35,7 @@ sealed class Subject : Parcelable {
abstract val title: String
abstract val notifyId: Int
abstract fun pendingIntent(context: Context): PendingIntent
abstract fun pendingIntent(context: Context): PendingIntent?
@Parcelize
class Module(
@@ -53,7 +53,7 @@ sealed class Subject : Parcelable {
override fun pendingIntent(context: Context) = when (action) {
Action.Flash -> FlashFragment.installIntent(context, file)
else -> Intent().toPending(context)
else -> null
}
}

View File

@@ -31,6 +31,7 @@ data class ModuleJson(
val version: String,
val versionCode: Int,
val zipUrl: String,
val changelog: String,
)
@JsonClass(generateAdapter = true)

View File

@@ -11,9 +11,10 @@ data class OnlineModule(
override var version: String,
override var versionCode: Int,
val zipUrl: String,
val changelog: String,
) : Module(), Parcelable {
constructor(local: LocalModule, json: ModuleJson) :
this(local.id, local.name, json.version, json.versionCode, json.zipUrl)
this(local.id, local.name, json.version, json.versionCode, json.zipUrl, json.changelog)
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()

View File

@@ -4,6 +4,7 @@ import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@@ -29,11 +30,8 @@ import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.coroutineScope
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.widget.IndeterminateCheckBox
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@BindingAdapter("gone")
@@ -57,10 +55,8 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
}
@BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, text: CharSequence) {
tv.coroutineScope.launch(Dispatchers.IO) {
ServiceLocator.markwon.setMarkdown(tv, text.toString())
}
fun setMarkdownText(tv: TextView, markdown: Spanned) {
ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
}
@BindingAdapter("onNavigationClick")

View File

@@ -1,16 +1,17 @@
package com.topjohnwu.magisk.di
import android.content.Context
import android.text.method.LinkMovementMethod
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.ktx.precomputedText
import com.topjohnwu.magisk.utils.MarkwonImagePlugin
import com.topjohnwu.magisk.core.utils.currentLocale
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.utils.NoCopySpannableFactory
import okhttp3.Cache
import okhttp3.ConnectionSpec
import okhttp3.Dns
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@@ -46,7 +47,8 @@ private class DnsResolver(client: OkHttpClient) : Dns {
if (Config.doh) {
try {
return doh.lookup(hostname)
} catch (e: UnknownHostException) {}
} catch (e: UnknownHostException) {
}
}
return Dns.SYSTEM.lookup(hostname)
}
@@ -61,11 +63,16 @@ fun createOkHttpClient(context: Context): OkHttpClient {
builder.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
})
} else {
builder.connectionSpecs(listOf(ConnectionSpec.RESTRICTED_TLS))
}
builder.dns(DnsResolver(builder.build()))
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
request.header("User-Agent", "Magisk ${BuildConfig.VERSION_CODE}")
request.header("User-Agent", "Magisk/${BuildConfig.VERSION_CODE}")
request.header("Accept-Language", currentLocale.toLanguageTag())
chain.proceed(request.build())
}
@@ -73,7 +80,6 @@ fun createOkHttpClient(context: Context): OkHttpClient {
Info.hasGMS = false
}
builder.dns(DnsResolver(builder.build()))
return builder.build()
}
@@ -96,13 +102,17 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
.create(T::class.java)
}
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
fun createMarkwon(context: Context): Markwon {
return Markwon.builder(context)
.textSetter { textView, spanned, _, onComplete ->
textView.tag = onComplete
textView.precomputedText = spanned
.textSetter { textView, spanned, bufferType, onComplete ->
textView.apply {
post {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
setText(spanned, bufferType)
onComplete.run()
}
}
}
.usePlugin(HtmlPlugin.create())
.usePlugin(MarkwonImagePlugin(okHttpClient))
.build()
}

View File

@@ -39,7 +39,7 @@ object ServiceLocator {
// Networking
val okhttp by lazy { createOkHttpClient(context) }
val retrofit by lazy { createRetrofit(okhttp) }
val markwon by lazy { createMarkwon(context, okhttp) }
val markwon by lazy { createMarkwon(context) }
val networkService by lazy {
NetworkService(
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),

View File

@@ -10,9 +10,8 @@ import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
import java.io.IOException
abstract class MarkDownDialog : DialogEvent() {
@@ -23,17 +22,13 @@ abstract class MarkDownDialog : DialogEvent() {
with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
setView(view)
(ownerActivity as BaseActivity).lifecycleScope.launch {
val tv = view.findViewById<TextView>(R.id.md_txt)
withContext(Dispatchers.IO) {
try {
ServiceLocator.markwon.setMarkdown(tv, getMarkdownText())
} catch (e: Exception) {
if (e is CancellationException)
throw e
Timber.e(e)
tv.post { tv.setText(R.string.download_file_error) }
}
val tv = view.findViewById<TextView>(R.id.md_txt)
(ownerActivity as BaseActivity).lifecycleScope.launch(Dispatchers.IO) {
try {
ServiceLocator.markwon.setMarkdown(tv, getMarkdownText())
} catch (e: IOException) {
Timber.e(e)
tv.post { tv.setText(R.string.download_file_error) }
}
}
}

View File

@@ -1,16 +1,24 @@
package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.MagiskDialog
class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String {
val str = svc.fetchString(item.changelog)
return if (str.length > 1000) str.substring(0, 1000) else str
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.apply {
fun download(install: Boolean) {
@@ -19,21 +27,21 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
DownloadService.start(context, subject)
}
setTitle(context.getString(R.string.repo_install_title, item.name))
setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
val title = context.getString(R.string.repo_install_title,
item.name, item.version, item.versionCode)
setTitle(title)
setCancelable(true)
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = R.string.download
icon = R.drawable.ic_download_md2
onClick { download(false) }
}
if (Info.env.isActive) {
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
icon = R.drawable.ic_install
onClick { download(true) }
}
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { download(true) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = android.R.string.cancel
}
}
}

View File

@@ -299,53 +299,6 @@ val View.activity: Activity get() {
}
}
var View.coroutineScope: CoroutineScope
get() = getTag(R.id.coroutineScope) as? CoroutineScope
?: (activity as? BaseActivity)?.lifecycleScope
?: GlobalScope
set(value) = setTag(R.id.coroutineScope, value)
@set:BindingAdapter("precomputedText")
var TextView.precomputedText: CharSequence
get() = text
set(value) {
val callback = tag as? Runnable
coroutineScope.launch(Dispatchers.IO) {
if (SDK_INT >= 29) {
// Internally PrecomputedTextCompat will use 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 = AppContext.resources.displayMetrics.density
return (this * scale + 0.5).toInt()
}
@SuppressLint("PrivateApi")
fun getProperty(key: String, def: String): String {
runCatching {

View File

@@ -4,12 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.ktx.coroutineScope
class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Binding>() {
@@ -19,9 +17,6 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
override fun onStart() {
super.onStart()
requireActivity().setTitle(R.string.install)
// Allow markwon to run in viewmodel scope
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
}
override fun onCreateView(

View File

@@ -1,6 +1,8 @@
package com.topjohnwu.magisk.ui.install
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
@@ -12,10 +14,12 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.events.MagiskInstallFileEvent
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
@@ -55,23 +59,23 @@ class InstallViewModel(
set(value) = set(value, field, { field = it }, BR.data)
@get:Bindable
var notes = ""
var notes: Spanned = SpannableStringBuilder()
set(value) = set(value, field, { field = it }, BR.notes)
init {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
try {
File(AppContext.cacheDir, "${BuildConfig.VERSION_CODE}.md").run {
notes = when {
exists() -> readText()
Const.Url.CHANGELOG_URL.isEmpty() -> ""
else -> {
val text = svc.fetchString(Const.Url.CHANGELOG_URL)
writeText(text)
text
}
val file = File(AppContext.cacheDir, "${BuildConfig.VERSION_CODE}.md")
val text = when {
file.exists() -> file.readText()
Const.Url.CHANGELOG_URL.isEmpty() -> ""
else -> {
val str = svc.fetchString(Const.Url.CHANGELOG_URL)
file.writeText(str)
str
}
}
notes = ServiceLocator.markwon.toMarkdown(text)
} catch (e: IOException) {
Timber.e(e)
}

View File

@@ -1,233 +0,0 @@
package com.topjohnwu.magisk.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.widget.TextView
import androidx.annotation.WorkerThread
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.superuser.internal.WaitRunnable
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.image.*
import io.noties.markwon.image.data.DataUriSchemeHandler
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
import kotlinx.coroutines.*
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) { _, props ->
val dest = ImageProps.DESTINATION.require(props)
val size = ImageProps.IMAGE_SIZE.get(props)
ImageSpan(dest, size)
}
}
@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
// Get TextView sizes before setText() to resize all images
val wr = WaitRunnable {
val width = tv.width - tv.paddingLeft - tv.paddingRight
spans.forEach { it.canvasWidth = width }
}
tv.post(wr)
runBlocking {
// Wait for drawable to be set
spans.forEach { it.await() }
// Wait for canvasWidth to be set
wr.waitUntilDone()
}
}
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 ImageSpan(
dest: String,
private val size: ImageSize?
) : DynamicDrawableSpan(ALIGN_BOTTOM) {
var canvasWidth = 0
private var measured = false
private lateinit var draw: Drawable
private val job: Job
init {
// Asynchronously download/decode images in the background
job = GlobalScope.launch(Dispatchers.IO) {
draw = loadDrawable(dest) ?: ShapeDrawable()
}
}
suspend fun await() = job.join()
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 || 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 = try {
SVG.getFromInputStream(inputStream)
} catch (e: SVGParseException) {
throw IllegalStateException("Exception decoding SVG", e)
}
val w = svg.documentWidth
val h = svg.documentHeight
if (w <= 0 || h <= 0) {
val picture = svg.renderToPicture()
return PictureDrawable(picture)
}
val density: Float = AppContext.resources.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(AppContext.resources, bitmap)
}
}
}

View File

@@ -26,12 +26,11 @@ object Notifications {
fun setup(context: Context) {
if (SDK_INT >= 26) {
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
mgr.createNotificationChannel(channel)
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
mgr.createNotificationChannel(channel)
val channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
val channel2 = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
mgr.createNotificationChannels(listOf(channel, channel2))
}
}