refactor: minor cleanup

This commit is contained in:
Canny 2022-12-15 17:28:18 +03:00
parent 74318f30fb
commit 79d4330408
No known key found for this signature in database
GPG Key ID: 395CCB0AA979F27B
14 changed files with 213 additions and 211 deletions

View File

@ -1,10 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.patcher.worker.PatcherWorker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.dsl.worker
import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module
val workerModule = module {
worker { PatcherWorker(androidContext(), get(), get(), get()) }
workerOf(::PatcherWorker)
}

View File

@ -38,7 +38,7 @@ class ManagerAPI(
return out
}
suspend fun downloadPatches() = withContext(Dispatchers.Default) {
suspend fun downloadPatches() = withContext(Dispatchers.IO) {
try {
val asset =
if (prefs.srcPatches!! == ghPatches) reVancedAPI.findAsset(ghPatches, ".jar")
@ -46,7 +46,6 @@ class ManagerAPI(
asset.run {
downloadAsset(app.cacheDir, downloadUrl).run {
patcherUtils.run {
patchBundleFile = absolutePath
loadPatchBundle(absolutePath)
}
}

View File

@ -7,11 +7,8 @@ import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.util.tag
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
@ -19,12 +16,10 @@ import io.sentry.Sentry
import java.util.*
class PatcherUtils(val app: Application) {
val patches = mutableStateOf<Resource<List<Class<out Patch<Context>>>>>(Resource.Loading)
val filteredPatches = mutableStateListOf<PatchClass>()
val patches = mutableStateOf<Resource<List<ReVancedPatch>>>(Resource.Loading)
val selectedAppPackage = mutableStateOf(Optional.empty<ApplicationInfo>())
val selectedAppPackagePath = mutableStateOf<String?>(null)
val selectedPatches = mutableStateListOf<String>()
lateinit var patchBundleFile: String
val selectedPatches = mutableStateListOf<ReVancedPatch>()
fun cleanup() {
patches.value = Resource.Loading
@ -32,16 +27,15 @@ class PatcherUtils(val app: Application) {
selectedPatches.clear()
}
fun loadPatchBundle(file: String? = patchBundleFile) {
fun loadPatchBundle(file: String) {
cleanup()
try {
if (this::patchBundleFile.isInitialized) {
val patchClasses = PatchBundle.Dex(
file!!, DexClassLoader(
file, DexClassLoader(
file, app.codeCacheDir.absolutePath, null, javaClass.classLoader
)
).loadPatches()
patches.value = Resource.Success(patchClasses)
} else throw IllegalStateException("No patch bundle(s) selected.")
} catch (e: Exception) {
Log.e(tag, "Failed to load patch bundle.", e)
Sentry.captureException(e)
@ -50,7 +44,9 @@ class PatcherUtils(val app: Application) {
fun getSelectedPackageInfo(): PackageInfo? {
return if (selectedAppPackage.value.isPresent) {
val path = selectedAppPackage.value.get().publicSourceDir ?: selectedAppPackagePath.value ?: return null
val path =
selectedAppPackage.value.get().publicSourceDir ?: selectedAppPackagePath.value
?: return null
app.packageManager.getPackageArchiveInfo(
path, 1
)
@ -58,9 +54,5 @@ class PatcherUtils(val app: Application) {
null
}
}
fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<Context>>> {
val (patches) = patches.value as? Resource.Success ?: return listOf()
return patches.filter { patch -> ids.any { it == patch.patchName } && patch.compatiblePackages!!.any { it.name == getSelectedPackageInfo()?.packageName } }
}
}
typealias ReVancedPatch = Class<out Patch<Context>>

View File

@ -13,6 +13,7 @@ import android.util.Log
import android.view.WindowManager
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
@ -80,20 +81,6 @@ class PatcherWorker(
Sentry.captureException(e)
}
return try {
runPatcher(workdir)
Result.success()
} catch (e: Exception) {
log("Error while patching: ${e::class.simpleName}: ${e.message}", ERROR)
Log.e(tag, e.stackTraceToString())
Sentry.captureException(e)
Result.failure()
}
}
private suspend fun runPatcher(
workdir: File
): Boolean {
val wakeLock: PowerManager.WakeLock =
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
@ -102,7 +89,8 @@ class PatcherWorker(
}
Log.d(tag, "Acquired wakelock.")
try {
return try {
val aaptPath = Aapt.binary(applicationContext)?.absolutePath
if (aaptPath == null) {
log("AAPT2 not found.", ERROR)
@ -118,13 +106,14 @@ class PatcherWorker(
val appPath = patcherUtils.selectedAppPackagePath.value
log("Checking prerequisites...", INFO)
val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
val patches = patcherUtils.selectedPatches
if (patches.isEmpty()) throw IllegalStateException("No patches selected.")
log("Creating directories...", INFO)
val inputFile = File(workdir, "input.apk")
val patchedFile = File(workdir, "patched.apk")
val outputFile = File(inputData.getString("output")!!)
val outputFile = File(workdir, "output.apk")
val finalFile = reVancedFolder.resolve(appInfo.packageName + ".apk")
val cacheDirectory = workdir.resolve("cache")
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
@ -203,19 +192,24 @@ class PatcherWorker(
withContext(Dispatchers.IO) {
Files.copy(
outputFile.inputStream(),
reVancedFolder.resolve(appInfo.packageName + ".apk").toPath(),
finalFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
log("Successfully patched!", SUCCESS)
patcherUtils.cleanup()
Result.success(Data.Builder().putString(OUTPUT, finalFile.absolutePath).build())
} catch (e: Exception) {
log("Error while patching: ${e::class.simpleName}: ${e.message}", ERROR)
Log.e(tag, e.stackTraceToString())
Sentry.captureException(e)
Result.failure()
} finally {
Log.d(tag, "Deleting workdir")
workdir.deleteRecursively()
wakeLock.release()
Log.d(tag, "Released wakelock.")
}
return false
}
@ -236,6 +230,7 @@ class PatcherWorker(
const val PATCH_MESSAGE = "PATCH_MESSAGE"
const val PATCH_STATUS = "PATCH_STATUS"
const val PATCH_LOG = "PATCH_LOG"
const val OUTPUT = "output"
const val INFO = 0
const val ERROR = 1

View File

@ -29,14 +29,12 @@ fun PatcherScreen(
onClickSourceSelector: () -> Unit,
vm: PatcherScreenViewModel = getViewModel(),
) {
val hasAppSelected by mutableStateOf(vm.selectedAppPackage.isPresent)
val patchesLoaded by mutableStateOf(vm.patchesLoaded is Resource.Success)
val context = LocalContext.current
Scaffold(
floatingActionButton = {
FloatingActionButton(
enabled = hasAppSelected && vm.selectedPatches.isNotEmpty(),
enabled = vm.selectedAppPackage.isPresent && vm.selectedPatches.isNotEmpty(),
onClick = onClickPatch,
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
text = { Text(stringResource(R.string.patch)) }
@ -66,7 +64,7 @@ fun PatcherScreen(
modifier = Modifier
.padding(vertical = 4.dp)
.fillMaxWidth(),
enabled = patchesLoaded,
enabled = vm.patches is Resource.Success,
onClick = onClickAppSelector
) {
Column(modifier = Modifier.padding(16.dp)) {
@ -86,7 +84,7 @@ fun PatcherScreen(
Spacer(Modifier.width(5.dp))
}
Text(
text = if (patchesLoaded) {
text = if (vm.patches is Resource.Success) {
if (vm.selectedAppPackage.isPresent) {
vm.selectedAppPackage.get().packageName
} else {
@ -105,7 +103,7 @@ fun PatcherScreen(
modifier = Modifier
.padding(vertical = 4.dp)
.fillMaxWidth(),
enabled = hasAppSelected,
enabled = vm.selectedAppPackage.isPresent,
onClick = onClickPatchSelector
) {
Column(modifier = Modifier.padding(16.dp)) {
@ -114,7 +112,7 @@ fun PatcherScreen(
style = MaterialTheme.typography.titleMedium
)
Text(
text = if (!hasAppSelected) {
text = if (!vm.selectedAppPackage.isPresent) {
stringResource(R.string.select_an_application_first)
} else if (vm.selectedPatches.isNotEmpty()) {
"${vm.selectedPatches.size} patches selected."

View File

@ -26,7 +26,7 @@ fun ContributorsSubscreen(
onBackClick: () -> Unit,
vm: ContributorsViewModel = getViewModel()
) {
val ctx = LocalContext.current.applicationContext
val ctx = LocalContext.current
AppScaffold(
topBar = { scrollBehavior ->
AppLargeTopBar(

View File

@ -15,12 +15,9 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppMediumTopBar
import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.PatchCard
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
@SuppressLint("UnrememberedMutableState")
@ -30,22 +27,14 @@ fun PatchesSelectorSubscreen(
onBackClick: () -> Unit,
vm: PatchesSelectorViewModel = getViewModel(),
) {
val patches = vm.filteredPatches
var query by mutableStateOf("")
LaunchedEffect(null) {
launch(Dispatchers.Default) {
vm.filterPatches()
}
}
AppScaffold(
topBar = { scrollBehavior ->
AppScaffold(topBar = { scrollBehavior ->
AppMediumTopBar(
topBarTitle = stringResource(id = R.string.card_patches_header),
scrollBehavior = scrollBehavior,
actions = {
IconButton(onClick = {
vm.selectAllPatches(patches, vm.selectedPatches.isEmpty())
vm.selectAllPatches(vm.patches, vm.selectedPatches.isEmpty())
}) {
if (vm.selectedPatches.isEmpty()) Icon(
Icons.Default.SelectAll, contentDescription = null
@ -54,13 +43,12 @@ fun PatchesSelectorSubscreen(
},
onBackClick = onBackClick
)
}
) { paddingValues ->
}) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues)
) {
if (!vm.loading) {
if (patches.isNotEmpty()) {
val search = vm.search
if (vm.patches.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
@ -74,17 +62,15 @@ fun PatchesSelectorSubscreen(
.fillMaxWidth()
.padding(8.dp),
shape = RoundedCornerShape(12.dp),
value = query,
onValueChange = { newValue ->
query = newValue
},
value = search,
onValueChange = { vm.search(it) },
leadingIcon = {
Icon(Icons.Default.Search, "Search")
},
trailingIcon = {
if (query.isNotEmpty()) {
if (search.isNotEmpty()) {
IconButton(onClick = {
query = ""
vm.clearSearch()
}) {
Icon(Icons.Default.Clear, "Clear")
}
@ -94,37 +80,26 @@ fun PatchesSelectorSubscreen(
}
}
LazyColumn(Modifier.padding(0.dp, 2.dp)) {
if (query.isEmpty() || query.isBlank()) {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
PatchCard(patch, vm.isPatchSelected(name)) {
vm.selectPatch(name, !vm.isPatchSelected(name))
}
}
} else {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
if (name.contains(query.lowercase())) {
PatchCard(patch, vm.isPatchSelected(name)) {
vm.selectPatch(name, !vm.isPatchSelected(name))
}
items(count = vm.patches.size) {
val patchClass = vm.patches[it]
val name = patchClass.patch.patchName
if (search.isBlank() && name.contains(search.lowercase())) {
PatchCard(patchClass, vm.isPatchSelected(patchClass.patch)) {
vm.selectPatch(
patchClass.patch,
!vm.isPatchSelected(patchClass.patch)
)
}
}
}
}
} else {
Column(
Modifier.fillMaxSize(),
Arrangement.Center,
Alignment.CenterHorizontally
Modifier.fillMaxSize(), Arrangement.Center, Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_compatible_patches))
}
}
} else LoadingIndicator(null)
}
}
}

View File

@ -53,7 +53,7 @@ fun PatchingSubscreen(
Column {
if (vm.installFailure) {
InstallFailureDialog(
onDismiss = { vm.installFailure = false },
onDismiss = { vm.dismissDialog() },
status = vm.pmStatus,
result = vm.extra
)
@ -149,7 +149,7 @@ fun PatchingSubscreen(
) {
Spacer(Modifier.weight(1f, true))
Button(onClick = {
vm.installApk(vm.outputFile)
vm.installApk()
}) {
Text(text = stringResource(R.string.install))
}

View File

@ -25,7 +25,6 @@ class AppSelectorViewModel(
val filteredApps = mutableStateListOf<ApplicationInfo>()
val patches = patcherUtils.patches
private val filteredPatches = patcherUtils.filteredPatches
private val selectedAppPackage = patcherUtils.selectedAppPackage
private val selectedAppPackagePath = patcherUtils.selectedAppPackagePath
private val selectedPatches = patcherUtils.selectedPatches
@ -34,15 +33,16 @@ class AppSelectorViewModel(
viewModelScope.launch { filterApps() }
}
private suspend fun filterApps() = withContext(Dispatchers.Default) {
@Suppress("RemoveExplicitTypeArguments")
private suspend fun filterApps() = withContext(Dispatchers.IO) {
try {
val (patches) = patches.value as Resource.Success
val apps = buildList<ApplicationInfo> {
patches.forEach patch@{ patch ->
patch.compatiblePackages?.forEach { pkg ->
try {
if (!(filteredApps.any { it.packageName == pkg.name })) {
val appInfo = app.packageManager.getApplicationInfo(pkg.name, 1)
filteredApps.add(appInfo)
if (!any { it.packageName == pkg.name }) {
add(app.packageManager.getApplicationInfo(pkg.name, 1))
return@forEach
}
} catch (e: Exception) {
@ -50,6 +50,10 @@ class AppSelectorViewModel(
}
}
}
}
withContext(Dispatchers.Main) {
filteredApps.addAll(apps)
}
Log.d(tag, "Filtered apps.")
} catch (e: Exception) {
Log.e(tag, "An error occurred while filtering", e)
@ -69,7 +73,6 @@ class AppSelectorViewModel(
selectedAppPackage.value.ifPresent { s ->
if (s != appId) {
selectedPatches.clear()
filteredPatches.clear()
}
}
selectedAppPackage.value = Optional.of(appId)
@ -86,8 +89,7 @@ class AppSelectorViewModel(
setSelectedAppPackage(
app.packageManager.getPackageArchiveInfo(
apkDir.path, 1
)!!.applicationInfo,
apkDir.absolutePath
)!!.applicationInfo, apkDir.absolutePath
)
} catch (e: Exception) {
Log.e(tag, "Failed to load apk", e)

View File

@ -21,5 +21,5 @@ class PatcherScreenViewModel(
val selectedPatches = patcherUtils.selectedPatches
val selectedAppPackage by patcherUtils.selectedAppPackage
val patchesLoaded by patcherUtils.patches
val patches by patcherUtils.patches
}

View File

@ -2,49 +2,64 @@ package app.revanced.manager.ui.viewmodel
import android.os.Parcelable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.patcher.ReVancedPatch
import app.revanced.manager.ui.Resource
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
class PatchesSelectorViewModel(
private val patcherUtils: PatcherUtils
) : ViewModel() {
val filteredPatches = patcherUtils.filteredPatches
val patches = mutableStateListOf<PatchClass>()
val selectedPatches = patcherUtils.selectedPatches
var loading by mutableStateOf(true)
var search by mutableStateOf("")
private set
fun isPatchSelected(patchId: String): Boolean {
return selectedPatches.contains(patchId)
fun search(search: String) {
this.search = search
}
fun selectPatch(patchId: String, state: Boolean) {
if (state) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
fun clearSearch() {
search = ""
}
init {
viewModelScope.launch { filterPatches() }
}
fun isPatchSelected(patch: ReVancedPatch): Boolean {
return selectedPatches.contains(patch)
}
fun selectPatch(patch: ReVancedPatch, state: Boolean) {
if (state) selectedPatches.add(patch)
else selectedPatches.remove(patch)
}
fun selectAllPatches(patchList: List<PatchClass>, selectAll: Boolean) {
patchList.forEach { patch ->
val patchId = patch.patch.patchName
if (selectAll && !patch.unsupported) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
patchList.forEach { patchClass ->
val patch = patchClass.patch
if (selectAll && !patchClass.unsupported) selectedPatches.add(patch)
else selectedPatches.remove(patch)
}
}
fun filterPatches() {
loading = true
val selected = patcherUtils.getSelectedPackageInfo() ?: return
val (patches) = patcherUtils.patches.value as? Resource.Success ?: return
if (filteredPatches.isNotEmpty()) {
loading = false; return
}
patches.forEach patch@{ patch ->
private suspend fun filterPatches() = withContext(Dispatchers.IO) {
val selected = patcherUtils.getSelectedPackageInfo() ?: return@withContext
val (patchList) = patcherUtils.patches.value as? Resource.Success ?: return@withContext
val filtered = buildList {
patchList.forEach { patch ->
var unsupported = false
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
@ -52,11 +67,14 @@ class PatchesSelectorViewModel(
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
filteredPatches.add(PatchClass(patch, unsupported))
add(PatchClass(patch, unsupported))
}
}
}
loading = false
}
withContext(Dispatchers.Main) {
patches.addAll(filtered)
}
}
}

View File

@ -17,6 +17,7 @@ import app.revanced.manager.installer.service.InstallService
import app.revanced.manager.installer.service.UninstallService
import app.revanced.manager.installer.utils.PM
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.util.toast
import java.io.File
class PatchingScreenViewModel(
@ -38,32 +39,43 @@ class PatchingScreenViewModel(
object Failure : Status()
}
val workManager = WorkManager.getInstance(app)
var installFailure by mutableStateOf(false)
var pmStatus by mutableStateOf(-999)
var extra by mutableStateOf("")
private var output: String? = null
val outputFile = File(app.cacheDir, "output.apk")
var installFailure by mutableStateOf(false)
private set
var pmStatus by mutableStateOf(-999)
private set
var extra by mutableStateOf("")
private set
private val workManager = WorkManager.getInstance(app)
private val patcherWorker =
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
Data.Builder().putString("output", outputFile.path).build()
Data.Builder().putString(PatcherWorker.OUTPUT, null).build()
).build()
private val liveData = workManager.getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData
private val liveData = workManager.getWorkInfoByIdLiveData(patcherWorker.id)
private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status
private val observer = Observer { workInfo: WorkInfo ->
status = when (workInfo.state) {
WorkInfo.State.RUNNING -> Status.Patching
WorkInfo.State.SUCCEEDED -> Status.Success
WorkInfo.State.FAILED -> Status.Failure
else -> Status.Idle
}
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
output = workInfo.outputData.getString("output")
}
}
val logs = mutableStateListOf<PatchLog>()
var status by mutableStateOf<Status>(Status.Idle)
private set
private val installBroadcastReceiver = object : BroadcastReceiver() {
@ -95,15 +107,25 @@ class PatchingScreenViewModel(
workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
liveData.observeForever(observer)
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION)
addAction(PatcherWorker.PATCH_LOG)
arrayOf(
InstallService.APP_INSTALL_ACTION,
UninstallService.APP_UNINSTALL_ACTION,
PatcherWorker.PATCH_LOG
).forEach {
addAction(it)
}
})
}
fun installApk(apk: File) {
PM.installApp(apk, app)
fun dismissDialog() {
installFailure = false
}
fun installApk() {
if (output != null) {
PM.installApp(File(output!!), app)
log(PatchLog.Info("Installing..."))
} else app.toast("Couldn't find APK file.")
}
fun postInstallStatus() {

View File

@ -7,11 +7,11 @@ import androidx.lifecycle.ViewModel
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.util.tag
import io.sentry.Sentry
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class SourceSelectorViewModel(val app: Application, val patcherUtils: PatcherUtils) : ViewModel() {
class SourceSelectorViewModel(val app: Application, private val patcherUtils: PatcherUtils) :
ViewModel() {
fun loadBundle(uri: Uri) {
try {
val patchesFile = app.cacheDir.resolve("patches.jar")
@ -20,10 +20,7 @@ class SourceSelectorViewModel(val app: Application, val patcherUtils: PatcherUti
patchesFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
patchesFile.absolutePath.also {
patcherUtils.patchBundleFile = it
patcherUtils.loadPatchBundle(it)
}
patcherUtils.loadPatchBundle(patchesFile.absolutePath)
} catch (e: Exception) {
Log.e(tag, "Failed to load bundle", e)
Sentry.captureException(e)

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.graphics.drawable.Drawable
import android.widget.Toast
import androidx.core.net.toUri
@ -20,3 +21,7 @@ fun Context.loadIcon(string: String): Drawable? {
null
}
}
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, string, duration).show()
}