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

@ -78,18 +78,13 @@ dependencies {
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
implementation("io.noties.markwon:core:4.6.2")
val vBAdapt = "4.0.0"
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
implementation("${bindingAdapter}:${vBAdapt}")
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
val vMarkwon = "4.6.2"
implementation("io.noties.markwon:core:${vMarkwon}")
implementation("io.noties.markwon:html:${vMarkwon}")
implementation("io.noties.markwon:image:${vMarkwon}")
implementation("com.caverock:androidsvg:1.4")
val vLibsu = "3.2.1"
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")

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,19 +22,15 @@ 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) {
(ownerActivity as BaseActivity).lifecycleScope.launch(Dispatchers.IO) {
try {
ServiceLocator.markwon.setMarkdown(tv, getMarkdownText())
} catch (e: Exception) {
if (e is CancellationException)
throw e
} 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.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()
val file = File(AppContext.cacheDir, "${BuildConfig.VERSION_CODE}.md")
val text = when {
file.exists() -> file.readText()
Const.Url.CHANGELOG_URL.isEmpty() -> ""
else -> {
val text = svc.fetchString(Const.Url.CHANGELOG_URL)
writeText(text)
text
}
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,
val channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
mgr.createNotificationChannel(channel)
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
val channel2 = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
mgr.createNotificationChannel(channel)
mgr.createNotificationChannels(listOf(channel, channel2))
}
}

View File

@ -223,11 +223,11 @@
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
gone="@{viewModel.notes.empty}"
gone="@{viewModel.notes.length == 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
@ -237,8 +237,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:visibility="gone"
tools:ignore="UnusedAttribute"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random"
tools:visibility="visible" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -9,7 +10,10 @@
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
android:paddingTop="10dp"
android:textAppearance="@style/AppearanceFoundation.Caption" />
android:textAppearance="@style/AppearanceFoundation.Caption"
tools:ignore="UnusedAttribute" />
</ScrollView>

View File

@ -174,7 +174,6 @@
<string name="yes">نعم</string>
<string name="no">لا</string>
<string name="repo_install_title">تثبيت %1$s</string>
<string name="repo_install_msg">هل تريد تثبيت %1$s ?</string>
<string name="download">تنزيل</string>
<string name="reboot">إعادة التشغيل</string>
<string name="release_notes">معلومات الأصدار الجديد</string>

View File

@ -44,7 +44,6 @@
<!--Toasts, Dialogs-->
<string name="repo_install_title">%1$s faylını yüklə</string>
<string name="repo_install_msg">%1$s faylını indi yükləmək istəyirsiniz?</string>
<string name="download">Yüklə</string>
<string name="reboot">Yenidən Başlat</string>
<string name="release_notes">Yeniliklər</string>

View File

@ -172,8 +172,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Так</string>
<string name="no">Не</string>
<string name="repo_install_title">Усталяваць %1$s</string>
<string name="repo_install_msg">Усталяваць %1$s?</string>
<string name="repo_install_title">Усталяваць %1$s %2$s(%3$s)</string>
<string name="download">Спампаваць</string>
<string name="reboot">Перазапуск</string>
<string name="release_notes">Пра выпуск</string>

View File

@ -36,8 +36,7 @@
<string name="app_changelog">Списък с промени</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Инсталиране на %1$s</string>
<string name="repo_install_msg">Желаете ли да инсталирате %1$s сега?</string>
<string name="repo_install_title">Инсталиране на %1$s %2$s(%3$s)</string>
<string name="download">Изтегляне</string>
<string name="reboot">Рестартиране</string>
<string name="magisk_update_title">Достъпно е издание на Magisk.</string>

View File

@ -186,8 +186,7 @@
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no">No</string>
<string name="repo_install_title">Instal·lar %1$s</string>
<string name="repo_install_msg">Vol instal·lar %1$s ara?</string>
<string name="repo_install_title">Instal·lar %1$s %2$s(%3$s)</string>
<string name="download">Baixar</string>
<string name="reboot">Reiniciar</string>
<string name="release_notes">Notes de llançament</string>

View File

@ -185,8 +185,7 @@
<!--Toasts, Dialogs-->
<string name="yes">ANO</string>
<string name="no">NE</string>
<string name="repo_install_title">Instalovat %1$s</string>
<string name="repo_install_msg">Chcete nyní nainstalovat %1$s?</string>
<string name="repo_install_title">Instalovat %1$s %2$s(%3$s)</string>
<string name="download">Stáhnout</string>
<string name="reboot">Restartovat</string>
<string name="release_notes">Poznámky k vydání</string>

View File

@ -194,7 +194,6 @@
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="repo_install_title">%1$s installieren</string>
<string name="repo_install_msg">Möchten Sie %1$s jetzt installieren?</string>
<string name="download">Herunterladen</string>
<string name="reboot">Neustart</string>
<string name="release_notes">Anmerkungen zur Veröffentlichung</string>

View File

@ -185,8 +185,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Ναι</string>
<string name="no">Όχι</string>
<string name="repo_install_title">Εγκατάσταση %1$s</string>
<string name="repo_install_msg">Θέλετε να εγκαταστήσετε το %1$s τώρα;</string>
<string name="repo_install_title">Εγκατάσταση %1$s %2$s(%3$s)</string>
<string name="download">Λήψη</string>
<string name="reboot">Επανεκκίνηση</string>
<string name="release_notes">Σημειώσεις έκδοσης</string>

View File

@ -195,8 +195,7 @@
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no">No</string>
<string name="repo_install_title">Instalar %1$s</string>
<string name="repo_install_msg">¿Desea instalar %1$s ahora?</string>
<string name="repo_install_title">Instalar %1$s %2$s(%3$s)</string>
<string name="download">Descargar</string>
<string name="reboot">Reiniciar</string>
<string name="release_notes">Notas de lanzamiento</string>

View File

@ -175,8 +175,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Jah</string>
<string name="no">Ei</string>
<string name="repo_install_title">Installi %1$s</string>
<string name="repo_install_msg">Kas soovid kohe installida %1$s?</string>
<string name="repo_install_title">Installi %1$s %2$s(%3$s)</string>
<string name="download">Allalaadimine</string>
<string name="reboot">Taaskäivita</string>
<string name="release_notes">Väljalaskemärkmed</string>

View File

@ -171,7 +171,6 @@
<string name="yes">بله</string>
<string name="no">نه</string>
<string name="repo_install_title">نصب کرد %1$s</string>
<string name="repo_install_msg">آیا می خواهید %1$s نصب شود؟</string>
<string name="download">انلود کردن</string>
<string name="reboot">راه اندازی مجدد</string>
<string name="release_notes">نکته های نسخه</string>

View File

@ -195,8 +195,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Oui</string>
<string name="no">Non</string>
<string name="repo_install_title">Installer %1$s</string>
<string name="repo_install_msg">Voulezvous installer %1$s maintenant?</string>
<string name="repo_install_title">Installer %1$s %2$s(%3$s)</string>
<string name="download">Télécharger</string>
<string name="reboot">Redémarrer</string>
<string name="release_notes">Notes de version</string>

View File

@ -177,8 +177,7 @@
<!--Toasts, Dialogs-->
<string name="yes">हाँ</string>
<string name="no">नहीं</string>
<string name="repo_install_title">इंस्टॉल %1$s</string>
<string name="repo_install_msg">क्या आप अभी %1$s इंस्टॉल करना चाहते हैं?</string>
<string name="repo_install_title">इंस्टॉल %1$s %2$s(%3$s)</string>
<string name="download">डाउनलोड</string>
<string name="reboot">रीबूट</string>
<string name="release_notes">रिलीज नोट्स</string>

View File

@ -176,8 +176,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="repo_install_title">Instaliraj %1$s</string>
<string name="repo_install_msg">Da li želite instalirati %1$s sada?</string>
<string name="repo_install_title">Instaliraj %1$s %2$s(%3$s)</string>
<string name="download">Preuzmi</string>
<string name="reboot">Ponovno pokreni</string>
<string name="release_notes">Bilješke o izdavanju aplikacije</string>

View File

@ -180,8 +180,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Ya</string>
<string name="no">Tidak</string>
<string name="repo_install_title">Instal %1$s</string>
<string name="repo_install_msg">Apakah Anda ingin menginstal %1$s sekarang?</string>
<string name="repo_install_title">Instal %1$s %2$s(%3$s)</string>
<string name="download">Download</string>
<string name="reboot">Nyalakan ulang</string>
<string name="release_notes">Catatan rilis</string>

View File

@ -193,8 +193,7 @@
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no">No</string>
<string name="repo_install_title">Installazione di %1$s</string>
<string name="repo_install_msg">Vuoi installare %1$s?</string>
<string name="repo_install_title">Installazione di %1$s %2$s(%3$s)</string>
<string name="download">Download</string>
<string name="reboot">Riavvia</string>
<string name="release_notes">Note di rilascio</string>

View File

@ -183,7 +183,6 @@
<string name="yes">כן</string>
<string name="no">לא</string>
<string name="repo_install_title">התקן %1$s</string>
<string name="repo_install_msg">האם ברצונך להתקין את %1$s כעת?</string>
<string name="download">הורדה</string>
<string name="reboot">הפעלה מחדש</string>
<string name="release_notes">הערות שחרור</string>

View File

@ -197,7 +197,6 @@
<string name="yes">対応</string>
<string name="no">非対応</string>
<string name="repo_install_title">%1$s をインストール</string>
<string name="repo_install_msg">%1$s をインストールしますか?</string>
<string name="download">ダウンロード</string>
<string name="reboot">再起動</string>
<string name="release_notes">更新履歴</string>

View File

@ -187,7 +187,6 @@
<string name="yes">დიახ</string>
<string name="no">არა</string>
<string name="repo_install_title">%1$s-ის ინსტალაცია</string>
<string name="repo_install_msg">გნებავთ %1$s-ის ახლავე დაინსტალირება?</string>
<string name="download">გადმოწერა</string>
<string name="reboot">გადატვირთვა</string>
<string name="release_notes">რელიზის შენიშვნები</string>

View File

@ -191,7 +191,6 @@
<string name="yes"></string>
<string name="no">아니오</string>
<string name="repo_install_title">%1$s 설치</string>
<string name="repo_install_msg">정말 %1$s을(를) 설치하시겠습니까?</string>
<string name="download">다운로드</string>
<string name="reboot">다시 시작</string>
<string name="release_notes">릴리즈 노트</string>

View File

@ -36,8 +36,7 @@
<string name="app_changelog">Pakeitimų sąrašas</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Instaliuoti %1$s</string>
<string name="repo_install_msg">Ar jūs norite instaliuoti %1$s?</string>
<string name="repo_install_title">Instaliuoti %1$s %2$s(%3$s)</string>
<string name="download">Atsisiųsti</string>
<string name="reboot">Perkrauti</string>
<string name="magisk_update_title">Atsirado nauja Magisk versija!</string>

View File

@ -52,8 +52,7 @@
<string name="reboot_delay_toast">Рестартирање за 5 секунди…</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Инсталирај %1$s</string>
<string name="repo_install_msg">Дали сакате да го инсталирате %1$s сега?</string>
<string name="repo_install_title">Инсталирај %1$s %2$s(%3$s)</string>
<string name="download">Преземи</string>
<string name="reboot">Рестартирај</string>
<string name="release_notes">Белешки за изданието</string>

View File

@ -186,8 +186,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Ja</string>
<string name="no">Nei</string>
<string name="repo_install_title">Installer %1$s</string>
<string name="repo_install_msg">Installer %1$s nå?</string>
<string name="repo_install_title">Installer %1$s %2$s(%3$s)</string>
<string name="download">Last ned</string>
<string name="reboot">Omstart</string>
<string name="release_notes">Utgivelsesnotater</string>

View File

@ -172,8 +172,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Ja</string>
<string name="no">Nee</string>
<string name="repo_install_title">%1$s installeren</string>
<string name="repo_install_msg">Wil je %1$s nu installeren?</string>
<string name="repo_install_title">%1$s installeren %2$s(%3$s)</string>
<string name="download">Downloaden</string>
<string name="reboot">Herstarten</string>
<string name="release_notes">Wijzigingslog</string>

View File

@ -177,8 +177,7 @@
<!--Toasts, Dialogs-->
<string name="yes">ਹਾਂ</string>
<string name="no">ਨਹੀਂ</string>
<string name="repo_install_title">ਇੰਸਟਾਲ %1$s</string>
<string name="repo_install_msg">ਕੀ ਤੁਸੀਂ ਹੁਣੇ %1$s ਇੰਸਟਾਲ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
<string name="repo_install_title">ਇੰਸਟਾਲ %1$s %2$s(%3$s)</string>
<string name="download">ਡਾਊਨਲੋਡ</string>
<string name="reboot">ਰੀਬੂਟ</string>
<string name="release_notes">ਰੀਲਿਜ਼ ਨੋਟਿਸ</string>

View File

@ -186,8 +186,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Tak</string>
<string name="no">Nie</string>
<string name="repo_install_title">Instalacja %1$s</string>
<string name="repo_install_msg">Czy chcesz teraz zainstalować %1$s ?</string>
<string name="repo_install_title">Instalacja %1$s %2$s(%3$s)</string>
<string name="download">Pobierz</string>
<string name="reboot">Reboot</string>
<string name="release_notes">Lista zmian</string>

View File

@ -190,8 +190,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Sim</string>
<string name="no">Não</string>
<string name="repo_install_title">Instalar %1$s</string>
<string name="repo_install_msg">Deseja instalar %1$s agora?</string>
<string name="repo_install_title">Instalar %1$s %2$s(%3$s)</string>
<string name="download">Download</string>
<string name="reboot">Reinicializar</string>
<string name="release_notes">Notas de versão</string>

View File

@ -32,8 +32,7 @@
<string name="app_changelog">Lista de alterações da aplicação</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Instalar %1$s</string>
<string name="repo_install_msg">Deseja instalar%1$s agora?</string>
<string name="repo_install_title">Instalar %1$s %2$s(%3$s)</string>
<string name="download">Transferir</string>
<string name="reboot">Reiniciar</string>
<string name="magisk_update_title">Nova atualização do Magisk disponível!</string>

View File

@ -193,8 +193,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Da</string>
<string name="no">Nu</string>
<string name="repo_install_title">Instalează %1$s</string>
<string name="repo_install_msg">Vrei să instalezi acum %1$s?</string>
<string name="repo_install_title">Instalează %1$s %2$s(%3$s)</string>
<string name="download">Descarcă</string>
<string name="reboot">Repornește</string>
<string name="release_notes">Note privind versiunea</string>

View File

@ -196,8 +196,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="repo_install_title">Установка %1$s</string>
<string name="repo_install_msg">Установить %1$s ?</string>
<string name="repo_install_title">Установка %1$s %2$s(%3$s)</string>
<string name="download">Скачать</string>
<string name="reboot">Перезагрузка</string>
<string name="release_notes">О версии</string>

View File

@ -196,8 +196,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Áno</string>
<string name="no">Nie</string>
<string name="repo_install_title">Nainštalovať %1$s</string>
<string name="repo_install_msg">Chcete teraz nainštalovať %1$s?</string>
<string name="repo_install_title">Nainštalovať %1$s %2$s(%3$s)</string>
<string name="download">Stiahnuť</string>
<string name="reboot">Reštartovať</string>
<string name="release_notes">Poznámky k vydaniu</string>

View File

@ -194,8 +194,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Po</string>
<string name="no">Jo</string>
<string name="repo_install_title">Instalo %1$s</string>
<string name="repo_install_msg">Dëshiron të instalosh %1$s tani?</string>
<string name="repo_install_title">Instalo %1$s %2$s(%3$s)</string>
<string name="download">Shkarko</string>
<string name="reboot">Rinis</string>
<string name="release_notes">Shënimet e lëshimit</string>

View File

@ -34,8 +34,7 @@
<string name="app_changelog">Дневник промена апликације</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Инсталирај %1$s</string>
<string name="repo_install_msg">Да ли желите да инсталирате %1$s?</string>
<string name="repo_install_title">Инсталирај %1$s %2$s(%3$s)</string>
<string name="download">Преузми</string>
<string name="reboot">Рестартуј</string>
<string name="magisk_update_title">Нови Адбејт Магиска Доступан!</string>

View File

@ -186,8 +186,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Ja</string>
<string name="no">Nej</string>
<string name="repo_install_title">Installera %1$s</string>
<string name="repo_install_msg">Vill du installera %1$s nu?</string>
<string name="repo_install_title">Installera %1$s %2$s(%3$s)</string>
<string name="download">Ladda ned</string>
<string name="reboot">Omstart</string>
<string name="release_notes">Utgivningsanmärkningar</string>

View File

@ -186,8 +186,7 @@
<!--Toasts, Dialogs-->
<string name="yes">ஆம்</string>
<string name="no">இல்லை</string>
<string name="repo_install_title">நிறுவு %1$s</string>
<string name="repo_install_msg">நீங்கள் இப்போது %1$s ஐ நிறுவ விரும்புகிறீர்களா??</string>
<string name="repo_install_title">நிறுவு %1$s %2$s(%3$s)</string>
<string name="download">பதிவிறக்கம்</string>
<string name="reboot">மறுதொடக்கம்</string>
<string name="release_notes">வெளியீட்டு குறிப்புகள்</string>

View File

@ -41,8 +41,7 @@
<string name="magisk_update_title">มีการอัพเดต Magisk!</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">ติดตั้ง %1$s</string>
<string name="repo_install_msg">ต้องการติดตั้ง %1$s ตอนนี้หรือไม่?</string>
<string name="repo_install_title">ติดตั้ง %1$s %2$s(%3$s)</string>
<string name="download">ดาวน์โหลด</string>
<string name="reboot">รีบู๊ต</string>
<string name="release_notes">-้อมูลเพิ่มเติม</string>

View File

@ -187,7 +187,6 @@
<string name="yes">Evet</string>
<string name="no">Hayır</string>
<string name="repo_install_title">%1$s yükle</string>
<string name="repo_install_msg">%1$s yüklensin mi?</string>
<string name="download">İndir</string>
<string name="reboot">Yeniden Başlat</string>
<string name="release_notes">Sürüm notları</string>

View File

@ -193,8 +193,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="repo_install_title">Встановити %1$s</string>
<string name="repo_install_msg">Бажаєте встановити %1$s?</string>
<string name="repo_install_title">Встановити %1$s %2$s(%3$s)</string>
<string name="download">Завантажити</string>
<string name="reboot">Перезавантажити</string>
<string name="release_notes">Особливості версії</string>

View File

@ -193,8 +193,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Đồng ý</string>
<string name="no">Không</string>
<string name="repo_install_title">Cài đặt %1$s</string>
<string name="repo_install_msg">Bạn có muốn cài đặt %1$s ngay bây giờ không?</string>
<string name="repo_install_title">Cài đặt %1$s %2$s(%3$s)</string>
<string name="download">Tải xuống</string>
<string name="reboot">Khởi động lại</string>
<string name="release_notes">Ghi chú bản phát hành</string>

View File

@ -196,8 +196,7 @@
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no"></string>
<string name="repo_install_title">安装 %1$s</string>
<string name="repo_install_msg">确认安装 %1$s</string>
<string name="repo_install_title">安装 %1$s %2$s(%3$s)</string>
<string name="download">下载</string>
<string name="reboot">重启</string>
<string name="release_notes">发布说明</string>

View File

@ -193,8 +193,7 @@
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no"></string>
<string name="repo_install_title">安裝 %1$s</string>
<string name="repo_install_msg">您現在想要安裝 %1$s 嗎?</string>
<string name="repo_install_title">安裝 %1$s %2$s(%3$s)</string>
<string name="download">下載</string>
<string name="reboot">重新啟動</string>
<string name="release_notes">發布說明</string>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="recyclerScrollListener" type="id" />
<item name="coroutineScope" type="id" />
</resources>

View File

@ -197,8 +197,7 @@
<!--Toasts, Dialogs-->
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="repo_install_title">Install %1$s</string>
<string name="repo_install_msg">Do you want to install %1$s now?</string>
<string name="repo_install_title">Install %1$s %2$s(%3$s)</string>
<string name="download">Download</string>
<string name="reboot">Reboot</string>
<string name="release_notes">Release notes</string>