mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-25 22:47:37 +00:00
Modernize Repo class for Magisk modules
- Use Kotlin - Use Room database - Use retrofit for networking - Use RxJava pipeline for repo updating
This commit is contained in:
parent
0c17ea5755
commit
42e7db8d13
@ -81,6 +81,7 @@ dependencies {
|
|||||||
def vRetrofit = "2.6.0"
|
def vRetrofit = "2.6.0"
|
||||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||||
|
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||||
|
|
||||||
def vOkHttp = '4.0.1'
|
def vOkHttp = '4.0.1'
|
||||||
@ -101,6 +102,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
def vRoom = "2.1.0"
|
def vRoom = "2.1.0"
|
||||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||||
|
kapt "androidx.room:room-compiler:${vRoom}"
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.browser:browser:1.0.0'
|
implementation 'androidx.browser:browser:1.0.0'
|
||||||
|
@ -13,6 +13,8 @@ import androidx.multidex.MultiDex
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.work.impl.WorkDatabase
|
import androidx.work.impl.WorkDatabase
|
||||||
import androidx.work.impl.WorkDatabase_Impl
|
import androidx.work.impl.WorkDatabase_Impl
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||||
import com.topjohnwu.magisk.di.koinModules
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager
|
import com.topjohnwu.magisk.utils.LocaleManager
|
||||||
import com.topjohnwu.magisk.utils.RootUtils
|
import com.topjohnwu.magisk.utils.RootUtils
|
||||||
@ -113,6 +115,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
|||||||
Room.setFactory {
|
Room.setFactory {
|
||||||
when (it) {
|
when (it) {
|
||||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||||
|
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val CUSTOM_CHANNEL = "custom_channel"
|
const val CUSTOM_CHANNEL = "custom_channel"
|
||||||
const val LOCALE = "locale"
|
const val LOCALE = "locale"
|
||||||
const val DARK_THEME = "dark_theme"
|
const val DARK_THEME = "dark_theme"
|
||||||
const val ETAG_KEY = "ETag"
|
|
||||||
const val REPO_ORDER = "repo_order"
|
const val REPO_ORDER = "repo_order"
|
||||||
const val SHOW_SYSTEM_APP = "show_system"
|
const val SHOW_SYSTEM_APP = "show_system"
|
||||||
const val DOWNLOAD_CACHE = "download_cache"
|
const val DOWNLOAD_CACHE = "download_cache"
|
||||||
@ -117,8 +116,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||||
var locale by preference(Key.LOCALE, "")
|
var locale by preference(Key.LOCALE, "")
|
||||||
@JvmStatic
|
|
||||||
var etagKey by preference(Key.ETAG_KEY, "")
|
|
||||||
|
|
||||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||||
@ -195,7 +192,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.delete()
|
config.delete()
|
||||||
remove(Key.ETAG_KEY)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,13 +53,7 @@ object Const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Url {
|
object Url {
|
||||||
@Deprecated("This shouldn't be used. There's literally no need for it")
|
|
||||||
const val REPO_URL =
|
|
||||||
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
|
|
||||||
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
|
|
||||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||||
const val MODULE_INSTALLER =
|
|
||||||
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
|
|
||||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||||
@ -67,16 +61,19 @@ object Const {
|
|||||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||||
@JvmField
|
@JvmField
|
||||||
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
||||||
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
|
|
||||||
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||||
|
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||||
|
|
||||||
private fun getRaw(where: String, name: String) =
|
private fun getRaw(where: String, name: String) =
|
||||||
"${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name"
|
"${GITHUB_RAW_URL}topjohnwu/magisk_files/$where/$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
// others
|
// others
|
||||||
const val LINK_KEY = "Link"
|
const val LINK_KEY = "Link"
|
||||||
const val IF_NONE_MATCH = "If-None-Match"
|
const val IF_NONE_MATCH = "If-None-Match"
|
||||||
|
const val ETAG_KEY = "ETag"
|
||||||
// intents
|
// intents
|
||||||
const val OPEN_SECTION = "section"
|
const val OPEN_SECTION = "section"
|
||||||
const val INTENT_SET_NAME = "filename"
|
const val INTENT_SET_NAME = "filename"
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package com.topjohnwu.magisk.data.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class RepoDao {
|
||||||
|
|
||||||
|
val repoIDSet: Set<String> get() = getRepoID().map { it.id }.toSet()
|
||||||
|
|
||||||
|
val repos: List<Repo> get() = getReposWithOrder(when (Config.repoOrder) {
|
||||||
|
Config.Value.ORDER_NAME -> "name COLLATE NOCASE"
|
||||||
|
Config.Value.ORDER_DATE -> "last_update DESC"
|
||||||
|
else -> ""
|
||||||
|
})
|
||||||
|
|
||||||
|
var etagKey: String
|
||||||
|
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||||
|
get() = etagRaw()?.key.orEmpty()
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
clearRepos()
|
||||||
|
clearEtag()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repos ORDER BY :order")
|
||||||
|
protected abstract fun getReposWithOrder(order: String): List<Repo>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun addRepo(repo: Repo)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repos WHERE id = :id")
|
||||||
|
abstract fun getRepo(id: String): Repo?
|
||||||
|
|
||||||
|
@Query("SELECT id FROM repos")
|
||||||
|
protected abstract fun getRepoID(): List<RepoID>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
abstract fun removeRepo(repo: Repo)
|
||||||
|
|
||||||
|
@Query("DELETE FROM repos WHERE id = :id")
|
||||||
|
abstract fun removeRepo(id: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||||
|
abstract fun removeRepos(idList: List<String>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM etag")
|
||||||
|
protected abstract fun etagRaw(): RepoEtag?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||||
|
|
||||||
|
@Query("DELETE FROM repos")
|
||||||
|
protected abstract fun clearRepos()
|
||||||
|
|
||||||
|
@Query("DELETE FROM etag")
|
||||||
|
protected abstract fun clearEtag()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RepoID(
|
||||||
|
@PrimaryKey val id: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(tableName = "etag")
|
||||||
|
data class RepoEtag(
|
||||||
|
@PrimaryKey val id: Int,
|
||||||
|
val key: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.data.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
|
||||||
|
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||||
|
abstract class RepoDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun repoDao() : RepoDao
|
||||||
|
}
|
@ -1,109 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.data.database
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.topjohnwu.magisk.Config
|
|
||||||
import com.topjohnwu.magisk.model.entity.Repo
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
class RepoDatabaseHelper
|
|
||||||
constructor(context: Context) : SQLiteOpenHelper(context, "repo.db", null, DATABASE_VER) {
|
|
||||||
|
|
||||||
private val mDb: SQLiteDatabase = writableDatabase
|
|
||||||
|
|
||||||
val rawCursor: Cursor
|
|
||||||
@Deprecated("")
|
|
||||||
get() = mDb.query(TABLE_NAME, null, null, null, null, null, null)
|
|
||||||
|
|
||||||
val repoCursor: Cursor
|
|
||||||
@Deprecated("")
|
|
||||||
get() {
|
|
||||||
var orderBy: String? = null
|
|
||||||
when (Config.repoOrder) {
|
|
||||||
Config.Value.ORDER_NAME -> orderBy = "name COLLATE NOCASE"
|
|
||||||
Config.Value.ORDER_DATE -> orderBy = "last_update DESC"
|
|
||||||
}
|
|
||||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
val repoIDSet: Set<String>
|
|
||||||
@Deprecated("")
|
|
||||||
get() {
|
|
||||||
val set = HashSet<String>(300)
|
|
||||||
mDb.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
set.add(c.getString(c.getColumnIndex("id")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
|
||||||
onUpgrade(db, 0, DATABASE_VER)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
||||||
if (oldVersion != newVersion) {
|
|
||||||
// Nuke old DB and create new table
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
|
|
||||||
db.execSQL(
|
|
||||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
|
||||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
|
||||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))")
|
|
||||||
Config.prefs.edit {
|
|
||||||
remove(Config.Key.ETAG_KEY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
||||||
onUpgrade(db, 0, DATABASE_VER)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun clearRepo() {
|
|
||||||
mDb.delete(TABLE_NAME, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun removeRepo(id: String) {
|
|
||||||
mDb.delete(TABLE_NAME, "id=?", arrayOf(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun removeRepo(repo: Repo) {
|
|
||||||
removeRepo(repo.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun removeRepo(list: Iterable<String>) {
|
|
||||||
list.forEach {
|
|
||||||
mDb.delete(TABLE_NAME, "id=?", arrayOf(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun addRepo(repo: Repo) {
|
|
||||||
mDb.replace(TABLE_NAME, null, repo.contentValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
fun getRepo(id: String): Repo? {
|
|
||||||
mDb.query(TABLE_NAME, null, "id=?", arrayOf(id), null, null, null).use { c ->
|
|
||||||
if (c.moveToNext()) {
|
|
||||||
return Repo(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATABASE_VER = 5
|
|
||||||
private val TABLE_NAME = "repos"
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,15 +2,14 @@ package com.topjohnwu.magisk.data.network
|
|||||||
|
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||||
|
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||||
|
import io.reactivex.Flowable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.http.GET
|
import retrofit2.adapter.rxjava2.Result
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.*
|
||||||
import retrofit2.http.Streaming
|
|
||||||
import retrofit2.http.Url
|
|
||||||
|
|
||||||
|
interface GithubRawServices {
|
||||||
interface GithubRawApiServices {
|
|
||||||
|
|
||||||
//region topjohnwu/magisk_files
|
//region topjohnwu/magisk_files
|
||||||
|
|
||||||
@ -41,6 +40,9 @@ interface GithubRawApiServices {
|
|||||||
@Streaming
|
@Streaming
|
||||||
fun fetchInstaller(): Single<ResponseBody>
|
fun fetchInstaller(): Single<ResponseBody>
|
||||||
|
|
||||||
|
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||||
|
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,6 +53,9 @@ interface GithubRawApiServices {
|
|||||||
@Streaming
|
@Streaming
|
||||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||||
|
|
||||||
|
@GET
|
||||||
|
fun fetchString(@Url url: String): Single<String>
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REVISION = "revision"
|
private const val REVISION = "revision"
|
||||||
@ -64,3 +69,13 @@ interface GithubRawApiServices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GithubApiServices {
|
||||||
|
|
||||||
|
@GET("repos")
|
||||||
|
fun fetchRepos(@Query("page") page: Int,
|
||||||
|
@Header(Const.Key.IF_NONE_MATCH) etag: String,
|
||||||
|
@Query("sort") sort: String = "pushed",
|
||||||
|
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||||
|
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
|
||||||
|
|
||||||
class FileRepository(
|
|
||||||
private val api: GithubRawApiServices
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun downloadFile(url: String) = api.fetchFile(url)
|
|
||||||
|
|
||||||
fun downloadInstaller() = api.fetchInstaller()
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.topjohnwu.magisk.App
|
import com.topjohnwu.magisk.App
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
import com.topjohnwu.magisk.data.database.base.su
|
import com.topjohnwu.magisk.data.database.base.su
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
@ -16,8 +15,7 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
class MagiskRepository(
|
class MagiskRepository(
|
||||||
private val context: Context,
|
private val apiRaw: GithubRawServices,
|
||||||
private val apiRaw: GithubRawApiServices,
|
|
||||||
private val packageManager: PackageManager
|
private val packageManager: PackageManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.topjohnwu.magisk.data.repository
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
|
||||||
|
class StringRepository(
|
||||||
|
private val api: GithubRawServices
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getString(url: String) = api.fetchString(url)
|
||||||
|
|
||||||
|
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||||
|
|
||||||
|
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
import com.topjohnwu.magisk.data.database.*
|
import com.topjohnwu.magisk.data.database.*
|
||||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
@ -10,6 +12,13 @@ val databaseModule = module {
|
|||||||
single { PolicyDao(get()) }
|
single { PolicyDao(get()) }
|
||||||
single { SettingsDao() }
|
single { SettingsDao() }
|
||||||
single { StringDao() }
|
single { StringDao() }
|
||||||
single { RepoDatabaseHelper(get()) }
|
single { createRepoDatabase(get()) }
|
||||||
single { UpdateRepos(get()) }
|
single { get<RepoDatabase>().repoDao() }
|
||||||
|
single { RepoUpdater(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createRepoDatabase(context: Context) =
|
||||||
|
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.build()
|
||||||
|
@ -4,20 +4,23 @@ import com.squareup.moshi.JsonAdapter
|
|||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||||
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||||
|
|
||||||
val networkingModule = module {
|
val networkingModule = module {
|
||||||
single { createOkHttpClient() }
|
single { createOkHttpClient() }
|
||||||
single { createMoshiConverterFactory() }
|
single { createMoshiConverterFactory() }
|
||||||
single { createRetrofit(get(), get()) }
|
single { createRetrofit(get(), get()) }
|
||||||
single { createApiService<GithubRawApiServices>(get(), Const.Url.GITHUB_RAW_API_URL) }
|
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||||
|
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOkHttpClient(): OkHttpClient {
|
fun createOkHttpClient(): OkHttpClient {
|
||||||
@ -45,6 +48,7 @@ fun createRetrofit(
|
|||||||
converterFactory: MoshiConverterFactory
|
converterFactory: MoshiConverterFactory
|
||||||
): Retrofit.Builder {
|
): Retrofit.Builder {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
|
.addConverterFactory(ScalarsConverterFactory.create())
|
||||||
.addConverterFactory(converterFactory)
|
.addConverterFactory(converterFactory)
|
||||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||||
import com.topjohnwu.magisk.data.repository.FileRepository
|
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||||
|
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
val repositoryModule = module {
|
val repositoryModule = module {
|
||||||
single { MagiskRepository(get(), get(), get()) }
|
single { MagiskRepository(get(), get()) }
|
||||||
single { LogRepository(get()) }
|
single { LogRepository(get()) }
|
||||||
single { AppRepository(get()) }
|
single { AppRepository(get()) }
|
||||||
single { FileRepository(get()) }
|
single { StringRepository(get()) }
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import com.skoumal.teanity.extensions.subscribeK
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.data.repository.FileRepository
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
|
||||||
@ -24,10 +24,10 @@ import java.io.InputStream
|
|||||||
|
|
||||||
abstract class RemoteFileService : NotificationService() {
|
abstract class RemoteFileService : NotificationService() {
|
||||||
|
|
||||||
private val repo by inject<FileRepository>()
|
private val service: GithubRawServices by inject()
|
||||||
|
|
||||||
private val supportedFolders
|
private val supportedFolders
|
||||||
get() = listOfNotNull(
|
get() = listOf(
|
||||||
cacheDir,
|
cacheDir,
|
||||||
Config.downloadDirectory
|
Config.downloadDirectory
|
||||||
)
|
)
|
||||||
@ -68,11 +68,11 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
|
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||||
.map { it.toStream(subject.hashCode()) }
|
.map { it.toStream(subject.hashCode()) }
|
||||||
.flatMap { stream ->
|
.flatMap { stream ->
|
||||||
when (subject) {
|
when (subject) {
|
||||||
is Module -> repo.downloadInstaller()
|
is Module -> service.fetchInstaller()
|
||||||
.map { stream.toModule(subject.file, it.byteStream()); subject.file }
|
.map { stream.toModule(subject.file, it.byteStream()); subject.file }
|
||||||
else -> Single.fromCallable { stream.writeTo(subject.file); subject.file }
|
else -> Single.fromCallable { stream.writeTo(subject.file); subject.file }
|
||||||
}
|
}
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.entity;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class OldBaseModule implements Comparable<OldBaseModule>, Parcelable {
|
|
||||||
|
|
||||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
|
||||||
private int mVersionCode = -1;
|
|
||||||
|
|
||||||
protected OldBaseModule() {
|
|
||||||
mId = mName = mVersion = mAuthor = mDescription = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected OldBaseModule(Cursor c) {
|
|
||||||
mId = nonNull(c.getString(c.getColumnIndex("id")));
|
|
||||||
mName = nonNull(c.getString(c.getColumnIndex("name")));
|
|
||||||
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
|
|
||||||
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
|
|
||||||
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
|
|
||||||
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected OldBaseModule(Parcel p) {
|
|
||||||
mId = p.readString();
|
|
||||||
mName = p.readString();
|
|
||||||
mVersion = p.readString();
|
|
||||||
mAuthor = p.readString();
|
|
||||||
mDescription = p.readString();
|
|
||||||
mVersionCode = p.readInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NonNull OldBaseModule module) {
|
|
||||||
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
dest.writeString(mId);
|
|
||||||
dest.writeString(mName);
|
|
||||||
dest.writeString(mVersion);
|
|
||||||
dest.writeString(mAuthor);
|
|
||||||
dest.writeString(mDescription);
|
|
||||||
dest.writeInt(mVersionCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String nonNull(String s) {
|
|
||||||
return s == null ? "" : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put("id", mId);
|
|
||||||
values.put("name", mName);
|
|
||||||
values.put("version", mVersion);
|
|
||||||
values.put("versionCode", mVersionCode);
|
|
||||||
values.put("author", mAuthor);
|
|
||||||
values.put("description", mDescription);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void parseProps(List<String> props) {
|
|
||||||
parseProps(props.toArray(new String[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void parseProps(String[] props) throws NumberFormatException {
|
|
||||||
for (String line : props) {
|
|
||||||
String[] prop = line.split("=", 2);
|
|
||||||
if (prop.length != 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
String key = prop[0].trim();
|
|
||||||
String value = prop[1].trim();
|
|
||||||
if (key.isEmpty() || key.charAt(0) == '#')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "id":
|
|
||||||
mId = value;
|
|
||||||
break;
|
|
||||||
case "name":
|
|
||||||
mName = value;
|
|
||||||
break;
|
|
||||||
case "version":
|
|
||||||
mVersion = value;
|
|
||||||
break;
|
|
||||||
case "versionCode":
|
|
||||||
mVersionCode = Integer.parseInt(value);
|
|
||||||
break;
|
|
||||||
case "author":
|
|
||||||
mAuthor = value;
|
|
||||||
break;
|
|
||||||
case "description":
|
|
||||||
mDescription = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
mName = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVersion() {
|
|
||||||
return mVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthor() {
|
|
||||||
return mAuthor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
mId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return mDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getVersionCode() {
|
|
||||||
return mVersionCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.entity;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.XStringKt;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class Repo extends OldBaseModule {
|
|
||||||
|
|
||||||
private Date mLastUpdate;
|
|
||||||
|
|
||||||
public Repo(String id) {
|
|
||||||
setId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repo(Cursor c) {
|
|
||||||
super(c);
|
|
||||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repo(Parcel p) {
|
|
||||||
super(p);
|
|
||||||
mLastUpdate = new Date(p.readLong());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Repo createFromParcel(Parcel source) {
|
|
||||||
return new Repo(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Repo[] newArray(int size) {
|
|
||||||
return new Repo[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
super.writeToParcel(dest, flags);
|
|
||||||
dest.writeLong(mLastUpdate.getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update() throws IllegalRepoException {
|
|
||||||
String[] props = Utils.INSTANCE.dlString(getPropUrl()).split("\\n");
|
|
||||||
try {
|
|
||||||
parseProps(props);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getVersionCode() < 0) {
|
|
||||||
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
|
||||||
mLastUpdate = lastUpdate;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = super.getContentValues();
|
|
||||||
values.put("last_update", mLastUpdate.getTime());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getZipUrl() {
|
|
||||||
return String.format(Const.Url.ZIP_URL, getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPropUrl() {
|
|
||||||
return getFileUrl("module.prop");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDetailUrl() {
|
|
||||||
return getFileUrl("README.md");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileUrl(String file) {
|
|
||||||
return String.format(Const.Url.FILE_URL, getId(), file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLastUpdateString() {
|
|
||||||
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getLastUpdate() {
|
|
||||||
return mLastUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDownloadFilename() {
|
|
||||||
return XStringKt.legalFilename(getName() + "-" + getVersion() + ".zip");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class IllegalRepoException extends Exception {
|
|
||||||
IllegalRepoException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import android.os.Parcelable
|
|||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskJson
|
import com.topjohnwu.magisk.model.entity.MagiskJson
|
||||||
import com.topjohnwu.magisk.model.entity.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import com.topjohnwu.magisk.utils.cachedFile
|
import com.topjohnwu.magisk.utils.cachedFile
|
||||||
import com.topjohnwu.magisk.utils.get
|
import com.topjohnwu.magisk.utils.get
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
import kotlinx.android.parcel.IgnoredOnParcel
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.module
|
||||||
|
|
||||||
|
abstract class BaseModule : Comparable<BaseModule> {
|
||||||
|
abstract var id: String
|
||||||
|
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<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)
|
||||||
|
}
|
@ -1,50 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.model.entity
|
package com.topjohnwu.magisk.model.entity.module
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.io.SuFile
|
||||||
|
|
||||||
abstract class BaseModule : Comparable<BaseModule> {
|
|
||||||
abstract var id: String
|
|
||||||
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<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)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Module(path: String) : BaseModule() {
|
class Module(path: String) : BaseModule() {
|
||||||
override var id: String = ""
|
override var id: String = ""
|
||||||
override var name: String = ""
|
override var name: String = ""
|
@ -0,0 +1,68 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.module
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||||
|
import com.topjohnwu.magisk.utils.get
|
||||||
|
import com.topjohnwu.magisk.utils.legalFilename
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity(tableName = "repos")
|
||||||
|
@Parcelize
|
||||||
|
data class Repo(
|
||||||
|
@PrimaryKey override var id: String,
|
||||||
|
override var name: String,
|
||||||
|
override var author: String,
|
||||||
|
override var version: String,
|
||||||
|
override var versionCode: Int,
|
||||||
|
override var description: String,
|
||||||
|
var last_update: Long
|
||||||
|
) : BaseModule(), Parcelable {
|
||||||
|
|
||||||
|
private val stringRepo: StringRepository get() = get()
|
||||||
|
|
||||||
|
val lastUpdate get() = Date(last_update)
|
||||||
|
|
||||||
|
val lastUpdateString: String get() =
|
||||||
|
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(lastUpdate)
|
||||||
|
|
||||||
|
val downloadFilename: String get() = "$name-$version.zip".legalFilename()
|
||||||
|
|
||||||
|
val readme get() = stringRepo.getReadme(this)
|
||||||
|
|
||||||
|
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||||
|
|
||||||
|
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||||
|
|
||||||
|
@Throws(IllegalRepoException::class)
|
||||||
|
fun update() {
|
||||||
|
val props = runCatching {
|
||||||
|
stringRepo.getMetadata(this).blockingGet()
|
||||||
|
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||||
|
}.getOrElse {
|
||||||
|
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.runCatching {
|
||||||
|
parseProps(this)
|
||||||
|
}.onFailure {
|
||||||
|
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionCode < 0) {
|
||||||
|
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalRepoException::class)
|
||||||
|
fun update(lastUpdate: Date) {
|
||||||
|
last_update = lastUpdate.time
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
class IllegalRepoException(message: String) : Exception(message)
|
||||||
|
}
|
@ -6,8 +6,8 @@ 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.Module
|
import com.topjohnwu.magisk.model.entity.module.Module
|
||||||
import com.topjohnwu.magisk.model.entity.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import com.topjohnwu.magisk.utils.get
|
import com.topjohnwu.magisk.utils.get
|
||||||
import com.topjohnwu.magisk.utils.toggle
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
|
||||||
@ -69,11 +69,7 @@ class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
|||||||
|
|
||||||
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 == other.item
|
||||||
&& item.lastUpdate == other.item.lastUpdate
|
|
||||||
&& item.versionCode == other.item.versionCode
|
|
||||||
&& item.description == other.item.description
|
|
||||||
&& item.detailUrl == other.item.detailUrl
|
|
||||||
|
|
||||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.model.events
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
import com.topjohnwu.magisk.model.entity.Policy
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
import com.topjohnwu.magisk.model.entity.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
|
||||||
|
|
||||||
|
95
app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt
Normal file
95
app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package com.topjohnwu.magisk.tasks
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDao
|
||||||
|
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import se.ansman.kotshi.JsonSerializable
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.HashSet
|
||||||
|
|
||||||
|
class RepoUpdater(
|
||||||
|
private val api: GithubApiServices,
|
||||||
|
private val repoDB: RepoDao
|
||||||
|
) {
|
||||||
|
private lateinit var cached: MutableSet<String>
|
||||||
|
|
||||||
|
private val dateFormat: SimpleDateFormat
|
||||||
|
get() {
|
||||||
|
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||||
|
format.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadRepos(repos: List<GithubRepoInfo>) = Flowable.fromIterable(repos)
|
||||||
|
.parallel().runOn(Schedulers.io()).map {
|
||||||
|
it.id to dateFormat.parse(it.pushed_at)!!
|
||||||
|
}.map {
|
||||||
|
// Skip submission
|
||||||
|
if (it.first == "submission")
|
||||||
|
return@map
|
||||||
|
(repoDB.getRepo(it.first)?.apply {
|
||||||
|
cached.remove(it.first)
|
||||||
|
} ?: Repo(it.first)).runCatching {
|
||||||
|
update(it.second)
|
||||||
|
repoDB.addRepo(this)
|
||||||
|
}.getOrElse { Timber.e(it) }
|
||||||
|
}.sequential()
|
||||||
|
|
||||||
|
private fun loadPage(page: Int, etag: String = ""): Flowable<Unit> =
|
||||||
|
api.fetchRepos(page, etag).flatMap {
|
||||||
|
it.error()?.also { throw it }
|
||||||
|
it.response()?.run {
|
||||||
|
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
||||||
|
throw CachedException()
|
||||||
|
|
||||||
|
if (page == 1)
|
||||||
|
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
|
||||||
|
|
||||||
|
val flow = loadRepos(body()!!)
|
||||||
|
if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
|
||||||
|
flow.mergeWith(loadPage(page + 1))
|
||||||
|
} else {
|
||||||
|
flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forcedReload() = Flowable.fromIterable(cached)
|
||||||
|
.parallel().runOn(Schedulers.io()).map {
|
||||||
|
runCatching {
|
||||||
|
Repo(it).update()
|
||||||
|
}.getOrElse { Timber.e(it) }
|
||||||
|
}.sequential()
|
||||||
|
|
||||||
|
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
|
||||||
|
|
||||||
|
operator fun invoke(forced: Boolean = false) : Flowable<Unit> {
|
||||||
|
cached = Collections.synchronizedSet(HashSet(repoDB.repoIDSet))
|
||||||
|
return loadPage(1, repoDB.etagKey).doOnComplete {
|
||||||
|
repoDB.removeRepos(cached.toList())
|
||||||
|
cached.clear()
|
||||||
|
}.onErrorResumeNext { it: Throwable ->
|
||||||
|
cached.clear()
|
||||||
|
if (it is CachedException) {
|
||||||
|
if (forced) forcedReload() else Flowable.empty()
|
||||||
|
} else {
|
||||||
|
Flowable.error(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CachedException : Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable
|
||||||
|
data class GithubRepoInfo(
|
||||||
|
@Json(name = "name") val id: String,
|
||||||
|
val pushed_at: String
|
||||||
|
)
|
@ -1,191 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.tasks;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Repo;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.net.Networking;
|
|
||||||
import com.topjohnwu.net.Request;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public class UpdateRepos {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final RepoDatabaseHelper repoDB;
|
|
||||||
private Set<String> cached;
|
|
||||||
private Queue<Pair<String, Date>> moduleQueue;
|
|
||||||
|
|
||||||
public UpdateRepos(@NonNull RepoDatabaseHelper repoDatabase) {
|
|
||||||
repoDB = repoDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runTasks(Runnable task) {
|
|
||||||
Future[] futures = new Future[App.THREAD_POOL.getMaximumPoolSize() - 1];
|
|
||||||
for (int i = 0; i < futures.length; ++i) {
|
|
||||||
futures[i] = App.THREAD_POOL.submit(task);
|
|
||||||
}
|
|
||||||
for (Future f : futures) {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
f.get();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
continue;
|
|
||||||
} catch (ExecutionException ignored) {
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static instance of (Simple)DateFormat is not threadsafe so in order to make it safe it needs
|
|
||||||
* to be created beforehand on the same thread where it'll be used.
|
|
||||||
* See https://stackoverflow.com/a/18383395
|
|
||||||
*/
|
|
||||||
private static SimpleDateFormat getDateFormat() {
|
|
||||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
|
||||||
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We sort repos by last push, which means that we only need to check whether the
|
|
||||||
* first page is updated to determine whether the online repo database is changed
|
|
||||||
*/
|
|
||||||
private boolean parsePage(int page) {
|
|
||||||
Request req = Networking.get(Utils.INSTANCE.fmt(Const.Url.REPO_URL, page + 1));
|
|
||||||
if (page == 0) {
|
|
||||||
String etag = Config.getEtagKey();
|
|
||||||
if (!etag.isEmpty())
|
|
||||||
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
|
|
||||||
}
|
|
||||||
Request.Result<JSONArray> res = req.execForJSONArray();
|
|
||||||
// JSON not updated
|
|
||||||
if (res.getCode() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
|
||||||
return false;
|
|
||||||
// Network error
|
|
||||||
if (res.getResult() == null) {
|
|
||||||
cached.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Current page is the last page
|
|
||||||
if (res.getResult().length() == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
SimpleDateFormat dateFormat = getDateFormat();
|
|
||||||
|
|
||||||
for (int i = 0; i < res.getResult().length(); i++) {
|
|
||||||
JSONObject rawRepo = res.getResult().getJSONObject(i);
|
|
||||||
String id = rawRepo.getString("name");
|
|
||||||
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
|
|
||||||
moduleQueue.offer(new Pair<>(id, date));
|
|
||||||
}
|
|
||||||
} catch (JSONException | ParseException e) {
|
|
||||||
// Should not happen, but if exception occurs, page load fails
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update ETAG
|
|
||||||
if (page == 0) {
|
|
||||||
String etag = res.getConnection().getHeaderField(Config.Key.ETAG_KEY);
|
|
||||||
if (etag != null) {
|
|
||||||
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
|
|
||||||
Config.setEtagKey(etag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String links = res.getConnection().getHeaderField(Const.Key.LINK_KEY);
|
|
||||||
return links == null || !links.contains("next") || parsePage(page + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadPages() {
|
|
||||||
if (!parsePage(0))
|
|
||||||
return false;
|
|
||||||
runTasks(() -> {
|
|
||||||
while (true) {
|
|
||||||
Pair<String, Date> pair = moduleQueue.poll();
|
|
||||||
if (pair == null)
|
|
||||||
return;
|
|
||||||
Repo repo = repoDB.getRepo(pair.first);
|
|
||||||
try {
|
|
||||||
if (repo == null)
|
|
||||||
repo = new Repo(pair.first);
|
|
||||||
else
|
|
||||||
cached.remove(pair.first);
|
|
||||||
repo.update(pair.second);
|
|
||||||
repoDB.addRepo(repo);
|
|
||||||
} catch (Repo.IllegalRepoException e) {
|
|
||||||
Logger.debug(e.getMessage());
|
|
||||||
repoDB.removeRepo(pair.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fullReload() {
|
|
||||||
Cursor c = repoDB.getRawCursor();
|
|
||||||
runTasks(() -> {
|
|
||||||
while (true) {
|
|
||||||
Repo repo;
|
|
||||||
synchronized (c) {
|
|
||||||
if (!c.moveToNext())
|
|
||||||
return;
|
|
||||||
repo = new Repo(c);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
repo.update();
|
|
||||||
repoDB.addRepo(repo);
|
|
||||||
} catch (Repo.IllegalRepoException e) {
|
|
||||||
Logger.debug(e.getMessage());
|
|
||||||
repoDB.removeRepo(repo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<Boolean> exec(boolean force) {
|
|
||||||
return Single.fromCallable(() -> {
|
|
||||||
cached = Collections.synchronizedSet(repoDB.getRepoIDSet());
|
|
||||||
moduleQueue = new ConcurrentLinkedQueue<>();
|
|
||||||
|
|
||||||
if (loadPages()) {
|
|
||||||
// The leftover cached means they are removed from online repo
|
|
||||||
repoDB.removeRepo(cached);
|
|
||||||
} else if (force) {
|
|
||||||
fullReload();
|
|
||||||
}
|
|
||||||
return force; // not important
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<Boolean> exec() {
|
|
||||||
return exec(false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,18 +9,16 @@ import com.skoumal.teanity.util.DiffObservableList
|
|||||||
import com.skoumal.teanity.util.KObservableField
|
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.RepoDao
|
||||||
import com.topjohnwu.magisk.model.entity.Module
|
import com.topjohnwu.magisk.model.entity.module.Module
|
||||||
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
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
|
||||||
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
||||||
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
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.RepoUpdater
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
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
|
||||||
@ -29,8 +27,8 @@ import me.tatarka.bindingcollectionadapter2.OnItemBind
|
|||||||
|
|
||||||
class ModuleViewModel(
|
class ModuleViewModel(
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val repoDatabase: RepoDatabaseHelper,
|
private val repoUpdater: RepoUpdater,
|
||||||
private val repoUpdater: UpdateRepos
|
private val repoDB: RepoDao
|
||||||
) : MagiskViewModel() {
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
val query = KObservableField("")
|
val query = KObservableField("")
|
||||||
@ -65,9 +63,10 @@ class ModuleViewModel(
|
|||||||
.toList()
|
.toList()
|
||||||
.map { it to itemsInstalled.calculateDiff(it) }
|
.map { it to itemsInstalled.calculateDiff(it) }
|
||||||
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
|
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
|
||||||
.flatMap { repoUpdater.exec(force) }
|
.toFlowable()
|
||||||
.flatMap { Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } } }
|
.flatMap { repoUpdater(force) }
|
||||||
.flattenAsFlowable { it }
|
.collect({}, {_, _ -> })
|
||||||
|
.flattenAsFlowable { repoDB.repos }
|
||||||
.map { RepoRvItem(it) }
|
.map { RepoRvItem(it) }
|
||||||
.toList()
|
.toList()
|
||||||
.doOnSuccess { allItems.update(it) }
|
.doOnSuccess { allItems.update(it) }
|
||||||
|
@ -11,7 +11,7 @@ import com.topjohnwu.magisk.Config
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.FragmentReposBinding
|
import com.topjohnwu.magisk.databinding.FragmentReposBinding
|
||||||
import com.topjohnwu.magisk.model.download.DownloadService
|
import com.topjohnwu.magisk.model.download.DownloadService
|
||||||
import com.topjohnwu.magisk.model.entity.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
||||||
@ -89,7 +89,7 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openChangelog(item: Repo) {
|
private fun openChangelog(item: Repo) {
|
||||||
MarkDownWindow.show(requireActivity(), null, item.detailUrl)
|
MarkDownWindow.show(requireActivity(), null, item.readme)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
|
@ -8,7 +8,6 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
@ -20,7 +19,7 @@ import com.topjohnwu.magisk.BuildConfig
|
|||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
import com.topjohnwu.magisk.data.database.RepoDao
|
||||||
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
|
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
|
||||||
import com.topjohnwu.magisk.model.observer.Observer
|
import com.topjohnwu.magisk.model.observer.Observer
|
||||||
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
|
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
|
||||||
@ -33,7 +32,7 @@ import java.io.File
|
|||||||
|
|
||||||
class SettingsFragment : BasePreferenceFragment() {
|
class SettingsFragment : BasePreferenceFragment() {
|
||||||
|
|
||||||
private val repoDatabase: RepoDatabaseHelper by inject()
|
private val repoDB: RepoDao by inject()
|
||||||
|
|
||||||
private lateinit var updateChannel: ListPreference
|
private lateinit var updateChannel: ListPreference
|
||||||
private lateinit var autoRes: ListPreference
|
private lateinit var autoRes: ListPreference
|
||||||
@ -76,10 +75,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference("clear").setOnPreferenceClickListener {
|
findPreference("clear").setOnPreferenceClickListener {
|
||||||
prefs.edit {
|
repoDB.clear()
|
||||||
remove(Config.Key.ETAG_KEY)
|
|
||||||
}
|
|
||||||
repoDatabase.clearRepo()
|
|
||||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ import android.content.Context
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.net.Networking
|
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||||
import com.topjohnwu.net.ResponseListener
|
import com.topjohnwu.magisk.utils.inject
|
||||||
|
import io.reactivex.Single
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import ru.noties.markwon.html.HtmlPlugin
|
import ru.noties.markwon.html.HtmlPlugin
|
||||||
import ru.noties.markwon.image.ImagesPlugin
|
import ru.noties.markwon.image.ImagesPlugin
|
||||||
@ -16,20 +18,22 @@ import java.util.*
|
|||||||
|
|
||||||
object MarkDownWindow {
|
object MarkDownWindow {
|
||||||
|
|
||||||
|
private val stringRepo: StringRepository by inject()
|
||||||
|
|
||||||
fun show(activity: Context, title: String?, url: String) {
|
fun show(activity: Context, title: String?, url: String) {
|
||||||
Networking.get(url).getAsString(Listener(activity, title))
|
show(activity, title, stringRepo.getString(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(activity: Context, title: String?, input: InputStream) {
|
fun show(activity: Context, title: String?, input: InputStream) {
|
||||||
Scanner(input, "UTF-8").use {
|
Single.just(Scanner(input, "UTF-8").apply { useDelimiter("\\A") })
|
||||||
it.useDelimiter("\\A")
|
.map { it.next() }
|
||||||
Listener(activity, title).onResponse(it.next())
|
.also {
|
||||||
|
show(activity, title, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class Listener(var activity: Context, var title: String?) : ResponseListener<String> {
|
fun show(activity: Context, title: String?, content: Single<String>) {
|
||||||
|
content.subscribeK {
|
||||||
override fun onResponse(md: String) {
|
|
||||||
val markwon = Markwon.builder(activity)
|
val markwon = Markwon.builder(activity)
|
||||||
.usePlugin(HtmlPlugin.create())
|
.usePlugin(HtmlPlugin.create())
|
||||||
.usePlugin(ImagesPlugin.create(activity))
|
.usePlugin(ImagesPlugin.create(activity))
|
||||||
@ -40,7 +44,7 @@ object MarkDownWindow {
|
|||||||
val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null)
|
val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null)
|
||||||
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
||||||
try {
|
try {
|
||||||
markwon.setMarkdown(tv, md)
|
markwon.setMarkdown(tv, it)
|
||||||
} catch (e: ExceptionInInitializerError) {
|
} catch (e: ExceptionInInitializerError) {
|
||||||
//Nothing we can do about this error other than show error message
|
//Nothing we can do about this error other than show error message
|
||||||
tv.setText(R.string.download_file_error)
|
tv.setText(R.string.download_file_error)
|
||||||
|
@ -47,7 +47,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"
|
||||||
@ -62,7 +62,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"
|
||||||
@ -78,7 +78,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/update_time"
|
app:layout_constraintBottom_toTopOf="@+id/update_time"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user