diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt index d9ada0970..529b4cdf3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt @@ -43,7 +43,7 @@ sealed class Subject : Parcelable { val action: Action, override val notifyId: Int = Notifications.nextId() ) : Subject() { - override val url: String get() = module.zip_url + override val url: String get() = module.zipUrl override val title: String get() = module.downloadFilename @IgnoredOnParcel diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt index bbfea49c1..1246bc5e3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt @@ -28,11 +28,9 @@ data class StubJson( @JsonClass(generateAdapter = true) data class ModuleJson( - val id: String, - val last_update: Long, - val prop_url: String, - val zip_url: String, - val notes_url: String + val version: String, + val versionCode: Int, + val zipUrl: String, ) @JsonClass(generateAdapter = true) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/module/LocalModule.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/LocalModule.kt index cd3d5eb12..f9b0a02a0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/module/LocalModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/LocalModule.kt @@ -1,21 +1,26 @@ package com.topjohnwu.magisk.core.model.module import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.IOException import java.util.* data class LocalModule( private val path: String, - override var id: String = "", - override var name: String = "", - override var author: String = "", - override var version: String = "", - override var versionCode: Int = -1, - override var description: String = "", ) : Module() { + override var id: String = "" + override var name: String = "" + override var version: String = "" + override var versionCode: Int = -1 + var author: String = "" + var description: String = "" + var updateJson: String = "" + var updateInfo: OnlineModule? = null private val removeFile = SuFile(path, "remove") private val disableFile = SuFile(path, "disable") @@ -66,6 +71,30 @@ data class LocalModule( } } + @Throws(NumberFormatException::class) + private fun parseProps(props: List) { + 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 + "updateJson" -> updateJson = value + } + } + } + init { runCatching { parseProps(Shell.su("dos2unix < $path/module.prop").exec().out) @@ -81,13 +110,28 @@ data class LocalModule( } } + suspend fun load():Boolean { + if (updateJson.isEmpty()) return false + + try { + val json = ServiceLocator.networkService.fetchModuleJson(updateJson) + if (json.versionCode > versionCode) { + updateInfo = OnlineModule(this, json) + return true + } + } catch (e: IOException) { + Timber.w(e) + } + return false + } + companion object { private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk" suspend fun installed() = withContext(Dispatchers.IO) { SuFile(Const.MAGISK_PATH) - .listFiles { _, name -> name != "lost+found" && name != ".core" } + .listFiles() .orEmpty() .filter { !it.isFile } .map { LocalModule("${Const.MAGISK_PATH}/${it.name}") } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt index 7a201d168..859ca643b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt @@ -5,37 +5,10 @@ abstract class Module : Comparable { protected set abstract var name: String protected set - abstract var author: 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) { - 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: Module) = name.compareTo(other.name, true) + override operator fun compareTo(other: Module) = id.compareTo(other.id) } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/module/OnlineModule.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/OnlineModule.kt index f3f8809ef..27cfb4100 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/module/OnlineModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/OnlineModule.kt @@ -1,65 +1,26 @@ package com.topjohnwu.magisk.core.model.module import android.os.Parcelable -import androidx.room.Entity -import androidx.room.PrimaryKey import com.topjohnwu.magisk.core.model.ModuleJson -import com.topjohnwu.magisk.di.ServiceLocator -import com.topjohnwu.magisk.ktx.legalFilename import kotlinx.parcelize.Parcelize -import java.text.DateFormat -import java.util.* -@Entity(tableName = "modules") @Parcelize data class OnlineModule( - @PrimaryKey override var id: String, - override var name: String = "", - override var author: String = "", - override var version: String = "", - override var versionCode: Int = -1, - override var description: String = "", - val last_update: Long, - val prop_url: String, - val zip_url: String, - val notes_url: String + override var id: String, + override var name: String, + override var version: String, + override var versionCode: Int, + val zipUrl: String, ) : Module(), Parcelable { + constructor(local: LocalModule, json: ModuleJson) : + this(local.id, local.name, json.version, json.versionCode, json.zipUrl) - private val svc get() = ServiceLocator.networkService - - constructor(info: ModuleJson) : this( - id = info.id, - last_update = info.last_update, - prop_url = info.prop_url, - zip_url = info.zip_url, - notes_url = info.notes_url - ) - - val lastUpdate get() = Date(last_update) - val lastUpdateString get() = DATE_FORMAT.format(lastUpdate) val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename() - suspend fun notes() = svc.fetchString(notes_url) - - @Throws(IllegalRepoException::class) - suspend fun load() { - try { - val rawProps = svc.fetchString(prop_url) - val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() } - parseProps(props) - } catch (e: Exception) { - throw IllegalRepoException("Repo [$id] parse error:", e) - } - - if (versionCode < 0) { - throw IllegalRepoException("Repo [$id] does not contain versionCode") - } - } - - class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause) - - companion object { - private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) - } - + private fun String.legalFilename() = replace(" ", "_") + .replace("'", "").replace("\"", "") + .replace("$", "").replace("`", "") + .replace("*", "").replace("/", "_") + .replace("#", "").replace("@", "") + .replace("\\", "_") } diff --git a/app/src/main/java/com/topjohnwu/magisk/data/network/NetworkServices.kt b/app/src/main/java/com/topjohnwu/magisk/data/network/NetworkServices.kt index 39f0683d1..c892b96d4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/network/NetworkServices.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/network/NetworkServices.kt @@ -1,6 +1,7 @@ package com.topjohnwu.magisk.data.network import com.topjohnwu.magisk.core.model.BranchInfo +import com.topjohnwu.magisk.core.model.ModuleJson import com.topjohnwu.magisk.core.model.UpdateInfo import okhttp3.ResponseBody import retrofit2.http.* @@ -37,6 +38,9 @@ interface RawServices { @GET suspend fun fetchString(@Url url: String): String + @GET + suspend fun fetchModuleJson(@Url url: String): ModuleJson + } interface GithubApiServices { diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/NetworkService.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/NetworkService.kt index b1662843f..b7830b5d7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/NetworkService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/NetworkService.kt @@ -65,6 +65,7 @@ class NetworkService( } suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) } suspend fun fetchString(url: String) = wrap { raw.fetchString(url) } + suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) } private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleRvItem.kt index b874d146c..98433e5c1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleRvItem.kt @@ -5,7 +5,6 @@ import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.model.module.LocalModule -import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.databinding.DiffRvItem import com.topjohnwu.magisk.databinding.ObservableDiffRvItem import com.topjohnwu.magisk.databinding.RvContainer @@ -16,62 +15,29 @@ object InstallModule : DiffRvItem() { override val layoutRes = R.layout.item_module_download } -class SectionTitle( - val title: Int, - _button: Int = 0, - _icon: Int = 0 -) : ObservableDiffRvItem() { - override val layoutRes = R.layout.item_section_md2 - - @get:Bindable - var button = _button - set(value) = set(value, field, { field = it }, BR.button) - - @get:Bindable - var icon = _icon - set(value) = set(value, field, { field = it }, BR.icon) - - @get:Bindable - var hasButton = _button != 0 && _icon != 0 - set(value) = set(value, field, { field = it }, BR.hasButton) -} - -class OnlineModuleRvItem( - override val item: OnlineModule -) : ObservableDiffRvItem(), RvContainer { - override val layoutRes: Int = R.layout.item_repo_md2 - - @get:Bindable - var progress = 0 - set(value) = set(value, field, { field = it }, BR.progress) - - var hasUpdate = false - - override fun itemSameAs(other: OnlineModuleRvItem): Boolean = item.id == other.item.id -} - class LocalModuleRvItem( override val item: LocalModule ) : ObservableDiffRvItem(), RvContainer { override val layoutRes = R.layout.item_module_md2 - @get:Bindable - var online: OnlineModule? = null - set(value) = set(value, field, { field = it }, BR.online) - @get:Bindable var isEnabled = item.enable - set(value) = set(value, field, { field = it }, BR.enabled) { + set(value) = set(value, field, { field = it }, BR.enabled, BR.updateReady) { item.enable = value } @get:Bindable var isRemoved = item.remove - set(value) = set(value, field, { field = it }, BR.removed) { + set(value) = set(value, field, { field = it }, BR.removed, BR.updateReady) { item.remove = value } + @get:Bindable + var updateReady: Boolean + get() = item.updateInfo != null && !isRemoved && isEnabled + set(_) = notifyPropertyChanged(BR.updateReady) + val isSuspended = (Info.isZygiskEnabled && item.isRiru) || (!Info.isZygiskEnabled && item.isZygisk) @@ -80,11 +46,9 @@ class LocalModuleRvItem( else R.string.suspend_text_zygisk.asText(R.string.zygisk.asText()) val isUpdated get() = item.updated - val isModified get() = isRemoved || isUpdated - fun delete(viewModel: ModuleViewModel) { + fun delete() { isRemoved = !isRemoved - viewModel.updateActiveState() } override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 2e1c6ac3a..7296121cb 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -1,63 +1,30 @@ package com.topjohnwu.magisk.ui.module -import androidx.databinding.Bindable -import androidx.databinding.ObservableArrayList import androidx.lifecycle.viewModelScope import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R import com.topjohnwu.magisk.arch.BaseViewModel -import com.topjohnwu.magisk.arch.Queryable import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.OnlineModule -import com.topjohnwu.magisk.databinding.* -import com.topjohnwu.magisk.events.OpenReadmeEvent +import com.topjohnwu.magisk.databinding.RvItem +import com.topjohnwu.magisk.databinding.adapterOf +import com.topjohnwu.magisk.databinding.diffListOf +import com.topjohnwu.magisk.databinding.itemBindingOf import com.topjohnwu.magisk.events.SelectModuleEvent import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog -import com.topjohnwu.magisk.ktx.addOnListChangedCallback -import com.topjohnwu.magisk.ktx.reboot import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList -class ModuleViewModel : BaseViewModel(), Queryable { +class ModuleViewModel : BaseViewModel() { - val bottomBarBarrierIds = - intArrayOf(R.id.module_info, R.id.module_remove) + val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove) - override val queryDelay = 1000L - - @get:Bindable - var isRemoteLoading = false - set(value) = set(value, field, { field = it }, BR.remoteLoading) - - @get:Bindable - var query = "" - set(value) = set(value, field, { field = it }, BR.query) { - submitQuery() - // Yes we do lie about the search being loaded - searchLoading = true - } - - @get:Bindable - var searchLoading = false - set(value) = set(value, field, { field = it }, BR.searchLoading) - - val itemsSearch = diffListOf() - val itemSearchBinding = itemBindingOf { - it.bindExtra(BR.viewModel, this) - } - - private val installSectionList = ObservableArrayList() private val itemsInstalled = diffListOf() - private val sectionInstalled = SectionTitle( - R.string.module_installed, - R.string.reboot, - R.drawable.ic_restart - ).also { it.hasButton = false } val adapter = adapterOf() val items = MergeObservableList() @@ -65,34 +32,19 @@ class ModuleViewModel : BaseViewModel(), Queryable { it.bindExtra(BR.viewModel, this) } - // --- - init { - itemsInstalled.addOnListChangedCallback( - onItemRangeInserted = { _, _, _ -> - if (installSectionList.isEmpty()) - installSectionList.add(sectionInstalled) - }, - onItemRangeRemoved = { list, _, _ -> - if (list.isEmpty()) - installSectionList.clear() - } - ) - if (Info.env.isActive) { items.insertItem(InstallModule) - .insertList(installSectionList) .insertList(itemsInstalled) } } - // --- - override fun refresh(): Job { return viewModelScope.launch { state = State.LOADING loadInstalled() state = State.LOADED + loadUpdateInfo() } } @@ -104,62 +56,21 @@ class ModuleViewModel : BaseViewModel(), Queryable { itemsInstalled.update(installed, diff) } - fun forceRefresh() { - itemsInstalled.clear() - refresh() - submitQuery() + private suspend fun loadUpdateInfo() { + withContext(Dispatchers.IO) { + itemsInstalled.forEach { + it.updateReady = it.item.load() + } + } } - // --- - - private suspend fun queryInternal(query: String): List { - return if (query.isBlank()) { - itemsSearch.clear() - listOf() + fun downloadPressed(item: OnlineModule?) = + if (item != null && isConnected.get()) { + withExternalRW { ModuleInstallDialog(item).publish() } } else { - withContext(Dispatchers.Default) { - itemsInstalled.filter { - it.item.id.contains(query, true) - || it.item.name.contains(query, true) - || it.item.description.contains(query, true) - } - } + SnackbarEvent(R.string.no_connection).publish() } - } - - override fun query() { - viewModelScope.launch { - val searched = queryInternal(query) - val diff = withContext(Dispatchers.Default) { - itemsSearch.calculateDiff(searched) - } - searchLoading = false - itemsSearch.update(searched, diff) - } - } - - // --- - - fun updateActiveState() { - sectionInstalled.hasButton = itemsInstalled.any { it.isModified } - } - - fun sectionPressed(item: SectionTitle) = when (item) { - sectionInstalled -> reboot() - else -> Unit - } - - // The following methods are not used, but kept for future integration - - fun downloadPressed(item: OnlineModule) = - if (isConnected.get()) withExternalRW { ModuleInstallDialog(item).publish() } - else { SnackbarEvent(R.string.no_connection).publish() } fun installPressed() = withExternalRW { SelectModuleEvent().publish() } - fun infoPressed(item: OnlineModule) = - if (isConnected.get()) OpenReadmeEvent(item).publish() - else SnackbarEvent(R.string.no_connection).publish() - - fun infoPressed(item: LocalModuleRvItem) { infoPressed(item.online ?: return) } } diff --git a/app/src/main/res/layout/fragment_module_md2.xml b/app/src/main/res/layout/fragment_module_md2.xml index 0de58b86e..c08c9283f 100644 --- a/app/src/main/res/layout/fragment_module_md2.xml +++ b/app/src/main/res/layout/fragment_module_md2.xml @@ -17,21 +17,10 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/include_module_filter.xml b/app/src/main/res/layout/include_module_filter.xml deleted file mode 100644 index 081ade2db..000000000 --- a/app/src/main/res/layout/include_module_filter.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_module_download.xml b/app/src/main/res/layout/item_module_download.xml index a57dbfe43..6f444bcd2 100644 --- a/app/src/main/res/layout/item_module_download.xml +++ b/app/src/main/res/layout/item_module_download.xml @@ -18,6 +18,7 @@ style="@style/WidgetFoundation.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/l_50" android:onClick="@{() -> viewModel.installPressed()}" android:text="@string/module_action_install_external" android:textAllCaps="false" diff --git a/app/src/main/res/layout/item_module_md2.xml b/app/src/main/res/layout/item_module_md2.xml index 1bf6f0923..602ffabcf 100644 --- a/app/src/main/res/layout/item_module_md2.xml +++ b/app/src/main/res/layout/item_module_md2.xml @@ -98,7 +98,7 @@ app:layout_constraintBottom_toBottomOf="@+id/module_version_author" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" - app:layout_constraintStart_toEndOf="@+id/module_info" + app:layout_constraintStart_toEndOf="@+id/module_update" app:layout_constraintTop_toTopOf="@+id/module_title" /> - + app:srcCompat="@drawable/ic_download_md2" />