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:
topjohnwu 2019-07-28 01:54:34 -07:00
parent 0c17ea5755
commit 42e7db8d13
30 changed files with 401 additions and 691 deletions

View File

@ -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'

View File

@ -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
} }
} }

View File

@ -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)
} }
} }

View File

@ -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"

View File

@ -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
)

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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>>>
}

View File

@ -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()
}

View File

@ -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
) { ) {

View File

@ -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")
}

View File

@ -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()

View File

@ -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)

View File

@ -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()) }
} }

View File

@ -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 }
} }

View 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 = ""

View File

@ -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)
}

View File

@ -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
} }

View File

@ -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

View 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
)

View File

@ -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);
}
}

View File

@ -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) }

View File

@ -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")

View File

@ -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
} }

View File

@ -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)

View File

@ -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"