Migrate Magisk Modules to Kotlin

This commit is contained in:
topjohnwu 2019-07-27 15:46:44 -07:00
parent cdaff5b39c
commit 0c17ea5755
9 changed files with 144 additions and 224 deletions

View File

@ -1,86 +1,111 @@
package com.topjohnwu.magisk.model.entity package com.topjohnwu.magisk.model.entity
import android.os.Parcelable
import androidx.annotation.AnyThread
import androidx.annotation.NonNull
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.base.su import com.topjohnwu.superuser.Shell
import io.reactivex.Single import com.topjohnwu.superuser.io.SuFile
import kotlinx.android.parcel.Parcelize
import okhttp3.ResponseBody
import java.io.File
interface MagiskModule : Parcelable { abstract class BaseModule : Comparable<BaseModule> {
val id: String abstract var id: String
val name: String protected set
val author: String abstract var name: String
val version: String protected set
val versionCode: String abstract var author: String
val description: String protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
}
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
} }
@Entity(tableName = "repos") class Module(path: String) : BaseModule() {
@Parcelize override var id: String = ""
data class Repository( override var name: String = ""
@PrimaryKey @NonNull override var author: String = ""
override val id: String, override var version: String = ""
override val name: String, override var versionCode: Int = -1
override val author: String, override var description: String = ""
override val version: String,
override val versionCode: String,
override val description: String,
val lastUpdate: Long
) : MagiskModule
@Parcelize private val removeFile: SuFile = SuFile(path, "remove")
data class Module( private val disableFile: SuFile = SuFile(path, "disable")
override val id: String, private val updateFile: SuFile = SuFile(path, "update")
override val name: String,
override val author: String,
override val version: String,
override val versionCode: String,
override val description: String,
val path: String
) : MagiskModule
@AnyThread val updated: Boolean = updateFile.exists()
fun File.toModule(): Single<Module> {
val path = "${Const.MAGISK_PATH}/$name" var enable: Boolean = !disableFile.exists()
return "dos2unix < $path/module.prop".su() set(enable) {
.map { it.first().toModule(path) } field = if (enable) {
disableFile.delete()
} else {
!disableFile.createNewFile()
}
}
var remove: Boolean = removeFile.exists()
set(remove) {
field = if (remove) {
removeFile.createNewFile()
} else {
!removeFile.delete()
}
}
init {
runCatching {
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
}
if (id.isEmpty()) {
val sep = path.lastIndexOf('/')
id = path.substring(sep + 1)
}
if (name.isEmpty()) {
name = id;
}
}
companion object {
@WorkerThread
fun loadModules(): List<Module> {
val moduleList = mutableListOf<Module>()
val path = SuFile(Const.MAGISK_PATH)
val modules =
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
for (file in modules) {
if (file.isFile) continue
val module = Module(Const.MAGISK_PATH + "/" + file.name)
moduleList.add(module)
}
return moduleList
}
}
} }
fun Map<String, String>.toModule(path: String): Module {
return Module(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
description = get("description").orEmpty(),
path = path
)
}
@WorkerThread
fun ResponseBody.toRepository(lastUpdate: Long) = string()
.split(Regex("\\n"))
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()
.toRepository(lastUpdate)
@AnyThread
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
description = get("description").orEmpty(),
lastUpdate = lastUpdate
)

View File

@ -9,16 +9,16 @@ import androidx.annotation.NonNull;
import java.util.List; import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable { public abstract class OldBaseModule implements Comparable<OldBaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription; private String mId, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1; private int mVersionCode = -1;
protected BaseModule() { protected OldBaseModule() {
mId = mName = mVersion = mAuthor = mDescription = ""; mId = mName = mVersion = mAuthor = mDescription = "";
} }
protected BaseModule(Cursor c) { protected OldBaseModule(Cursor c) {
mId = nonNull(c.getString(c.getColumnIndex("id"))); mId = nonNull(c.getString(c.getColumnIndex("id")));
mName = nonNull(c.getString(c.getColumnIndex("name"))); mName = nonNull(c.getString(c.getColumnIndex("name")));
mVersion = nonNull(c.getString(c.getColumnIndex("version"))); mVersion = nonNull(c.getString(c.getColumnIndex("version")));
@ -27,7 +27,7 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
mDescription = nonNull(c.getString(c.getColumnIndex("description"))); mDescription = nonNull(c.getString(c.getColumnIndex("description")));
} }
protected BaseModule(Parcel p) { protected OldBaseModule(Parcel p) {
mId = p.readString(); mId = p.readString();
mName = p.readString(); mName = p.readString();
mVersion = p.readString(); mVersion = p.readString();
@ -36,17 +36,8 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
mVersionCode = p.readInt(); mVersionCode = p.readInt();
} }
protected BaseModule(MagiskModule m) {
mId = m.getId();
mName = m.getName();
mVersion = m.getVersion();
mAuthor = m.getAuthor();
mDescription = m.getDescription();
mVersionCode = Integer.parseInt(m.getVersionCode());
}
@Override @Override
public int compareTo(@NonNull BaseModule module) { public int compareTo(@NonNull OldBaseModule module) {
return getName().toLowerCase().compareTo(module.getName().toLowerCase()); return getName().toLowerCase().compareTo(module.getName().toLowerCase());
} }

View File

@ -1,83 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
public class OldModule extends BaseModule {
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
/* It won't be used at any place */
@Override
public OldModule createFromParcel(Parcel source) {
return null;
}
@Override
public OldModule[] newArray(int size) {
return null;
}
};
private final SuFile mRemoveFile;
private final SuFile mDisableFile;
private final SuFile mUpdateFile;
private final boolean mUpdated;
private boolean mEnable;
private boolean mRemove;
public OldModule(String path) {
try {
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
} catch (NumberFormatException ignored) {
}
mRemoveFile = new SuFile(path, "remove");
mDisableFile = new SuFile(path, "disable");
mUpdateFile = new SuFile(path, "update");
if (getId().isEmpty()) {
int sep = path.lastIndexOf('/');
setId(path.substring(sep + 1));
}
if (getName().isEmpty()) {
setName(getId());
}
mEnable = !mDisableFile.exists();
mRemove = mRemoveFile.exists();
mUpdated = mUpdateFile.exists();
}
public void createDisableFile() {
mEnable = !mDisableFile.createNewFile();
}
public void removeDisableFile() {
mEnable = mDisableFile.delete();
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile() {
mRemove = mRemoveFile.createNewFile();
}
public void deleteRemoveFile() {
mRemove = !mRemoveFile.delete();
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}

View File

@ -12,7 +12,7 @@ import com.topjohnwu.magisk.utils.XStringKt;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Date; import java.util.Date;
public class Repo extends BaseModule { public class Repo extends OldBaseModule {
private Date mLastUpdate; private Date mLastUpdate;
@ -30,11 +30,6 @@ public class Repo extends BaseModule {
mLastUpdate = new Date(p.readLong()); mLastUpdate = new Date(p.readLong());
} }
public Repo(Repository repo) {
super(repo);
mLastUpdate = new Date(repo.getLastUpdate());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() { public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override @Override
@ -107,7 +102,7 @@ public class Repo extends BaseModule {
return XStringKt.legalFilename(getName() + "-" + getVersion() + ".zip"); return XStringKt.legalFilename(getName() + "-" + getVersion() + ".zip");
} }
public class IllegalRepoException extends Exception { public static class IllegalRepoException extends Exception {
IllegalRepoException(String message) { IllegalRepoException(String message) {
super(message); super(message);
} }

View File

@ -6,44 +6,54 @@ import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.OldModule import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.Repo import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.Repository
import com.topjohnwu.magisk.utils.get import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.toggle import com.topjohnwu.magisk.utils.toggle
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() { class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module override val layoutRes: Int = R.layout.item_module
val lastActionNotice = KObservableField("") val lastActionNotice = KObservableField("")
val isChecked = KObservableField(item.isEnabled) val isChecked = KObservableField(item.enable)
val isDeletable = KObservableField(item.willBeRemoved()) val isDeletable = KObservableField(item.remove)
init { init {
isChecked.addOnPropertyChangedCallback { isChecked.addOnPropertyChangedCallback {
when (it) { when (it) {
true -> item.removeDisableFile().notice(R.string.disable_file_removed) true -> {
false -> item.createDisableFile().notice(R.string.disable_file_created) item.enable = true
notice(R.string.disable_file_removed)
}
false -> {
item.enable = false
notice(R.string.disable_file_created)
}
} }
} }
isDeletable.addOnPropertyChangedCallback { isDeletable.addOnPropertyChangedCallback {
when (it) { when (it) {
true -> item.createRemoveFile().notice(R.string.remove_file_created) true -> {
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted) item.remove = true
notice(R.string.remove_file_created)
}
false -> {
item.remove = false
notice(R.string.remove_file_deleted)
}
} }
} }
when { when {
item.isUpdated -> notice(R.string.update_file_created) item.updated -> notice(R.string.update_file_created)
item.willBeRemoved() -> notice(R.string.remove_file_created) item.remove -> notice(R.string.remove_file_created)
} }
} }
fun toggle() = isChecked.toggle() fun toggle() = isChecked.toggle()
fun toggleDelete() = isDeletable.toggle() fun toggleDelete() = isDeletable.toggle()
@Suppress("unused") private fun notice(@StringRes info: Int) {
private fun Any.notice(@StringRes info: Int) {
lastActionNotice.value = get<Resources>().getString(info) lastActionNotice.value = get<Resources>().getString(info)
} }
@ -57,8 +67,6 @@ class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() { class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
constructor(repo: Repository) : this(Repo(repo))
override val layoutRes: Int = R.layout.item_repo override val layoutRes: Int = R.layout.item_repo
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version

View File

@ -1,7 +1,6 @@
package com.topjohnwu.magisk.ui.module package com.topjohnwu.magisk.ui.module
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor
import com.skoumal.teanity.databinding.ComparableRvItem import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSuccessUi import com.skoumal.teanity.extensions.doOnSuccessUi
@ -11,6 +10,7 @@ import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.Repo import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
@ -20,7 +20,7 @@ import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.UpdateRepos import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.ui.base.MagiskViewModel import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.toList
import com.topjohnwu.magisk.utils.toSingle import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update import com.topjohnwu.magisk.utils.update
import io.reactivex.Single import io.reactivex.Single
@ -59,8 +59,7 @@ class ModuleViewModel(
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish() fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh(force: Boolean) { fun refresh(force: Boolean) {
Single.fromCallable { Utils.loadModulesLeanback() } Single.fromCallable { Module.loadModules() }
.map { it.values.toList() }
.flattenAsFlowable { it } .flattenAsFlowable { it }
.map { ModuleRvItem(it) } .map { ModuleRvItem(it) }
.toList() .toList()
@ -110,12 +109,6 @@ class ModuleViewModel(
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed) groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed)
} }
private fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
val out = mutableListOf<Result>()
while (moveToNext()) out.add(transformer(this))
return out
}
companion object { companion object {
protected const val MODULE_INSTALLED = 0 protected const val MODULE_INSTALLED = 0
protected const val MODULE_REMOTE = 1 protected const val MODULE_REMOTE = 1

View File

@ -10,16 +10,13 @@ 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
import androidx.annotation.WorkerThread
import androidx.work.* import androidx.work.*
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.OldModule
import com.topjohnwu.magisk.model.update.UpdateCheckService import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.net.Networking import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
import java.io.File import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -70,19 +67,6 @@ object Utils {
return info.loadLabel(pm).toString() return info.loadLabel(pm).toString()
} }
@WorkerThread
fun loadModulesLeanback(): Map<String, OldModule> {
val moduleMap = ValueSortedMap<String, OldModule>()
val path = SuFile(Const.MAGISK_PATH)
val modules = path.listFiles { _, name -> name != "lost+found" && name != ".core" }
for (file in modules!!) {
if (file.isFile) continue
val module = OldModule(Const.MAGISK_PATH + "/" + file.name)
moduleMap[module.id] = module
}
return moduleMap
}
fun showSuperUser(): Boolean { fun showSuperUser(): Boolean {
return Shell.rootAccess() && (Const.USER_ID == 0 return Shell.rootAccess() && (Const.USER_ID == 0
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED) || Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)

View File

@ -7,6 +7,7 @@ import android.content.pm.ComponentInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.* import android.content.pm.PackageManager.*
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import com.topjohnwu.magisk.App import com.topjohnwu.magisk.App
@ -104,3 +105,9 @@ fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title) fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name) fun Context.cachedFile(name: String) = File(cacheDir, name)
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
val out = mutableListOf<Result>()
while (moveToNext()) out.add(transformer(this))
return out
}

View File

@ -48,7 +48,7 @@
android:id="@+id/version_name" android:id="@+id/version_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{item.item.version == null || item.item.version.length == 0 ? @string/no_info_provided : item.item.version}" android:text="@{item.item.version.length == 0 ? @string/no_info_provided : item.item.version}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark" android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false" android:textIsSelectable="false"
@ -63,7 +63,7 @@
android:id="@+id/author" android:id="@+id/author"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{item.item.author == null || item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}" android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark" android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false" android:textIsSelectable="false"
@ -79,7 +79,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:text="@{item.item.description == null || item.item.description.length == 0 ? @string/no_info_provided : item.item.description}" android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/notice" app:layout_constraintBottom_toTopOf="@+id/notice"