diff --git a/app/build.gradle b/app/build.gradle index c092650bc..fff945444 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,6 +81,7 @@ dependencies { def vRetrofit = "2.6.0" implementation "com.squareup.retrofit2:retrofit:${vRetrofit}" implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" + implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" def vOkHttp = '4.0.1' @@ -101,6 +102,7 @@ dependencies { } def vRoom = "2.1.0" implementation "com.github.topjohnwu:room-runtime:${vRoom}" + kapt "androidx.room:room-compiler:${vRoom}" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.browser:browser:1.0.0' diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/App.kt index 1feb6d8a1..863d0a8b3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/App.kt @@ -13,6 +13,8 @@ import androidx.multidex.MultiDex import androidx.room.Room import androidx.work.impl.WorkDatabase 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.utils.LocaleManager import com.topjohnwu.magisk.utils.RootUtils @@ -113,6 +115,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks { Room.setFactory { when (it) { WorkDatabase::class.java -> WorkDatabase_Impl() + RepoDatabase::class.java -> RepoDatabase_Impl() else -> null } } diff --git a/app/src/main/java/com/topjohnwu/magisk/Config.kt b/app/src/main/java/com/topjohnwu/magisk/Config.kt index 6f08cbb52..72b7c0eec 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Config.kt +++ b/app/src/main/java/com/topjohnwu/magisk/Config.kt @@ -41,7 +41,6 @@ object Config : PreferenceModel, DBConfig { const val CUSTOM_CHANNEL = "custom_channel" const val LOCALE = "locale" const val DARK_THEME = "dark_theme" - const val ETAG_KEY = "ETag" const val REPO_ORDER = "repo_order" const val SHOW_SYSTEM_APP = "show_system" const val DOWNLOAD_CACHE = "download_cache" @@ -117,8 +116,6 @@ object Config : PreferenceModel, DBConfig { var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "") 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 suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER) @@ -195,7 +192,6 @@ object Config : PreferenceModel, DBConfig { } } config.delete() - remove(Key.ETAG_KEY) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/Const.kt b/app/src/main/java/com/topjohnwu/magisk/Const.kt index 694ca87c2..27657780a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Const.kt +++ b/app/src/main/java/com/topjohnwu/magisk/Const.kt @@ -53,13 +53,7 @@ object Const { } 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 MODULE_INSTALLER = - "https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh" const val PAYPAL_URL = "https://www.paypal.me/topjohnwu" const val PATREON_URL = "https://www.patreon.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" @JvmField 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) = - "${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name" + "${GITHUB_RAW_URL}topjohnwu/magisk_files/$where/$name" } object Key { // others const val LINK_KEY = "Link" const val IF_NONE_MATCH = "If-None-Match" + const val ETAG_KEY = "ETag" // intents const val OPEN_SECTION = "section" const val INTENT_SET_NAME = "filename" diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt new file mode 100644 index 000000000..b667a0734 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt @@ -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 get() = getRepoID().map { it.id }.toSet() + + val repos: List 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 + + @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 + + @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) + + @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 +) + diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabase.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabase.kt new file mode 100644 index 000000000..c7ad3a00e --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabase.kt @@ -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 +} diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabaseHelper.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabaseHelper.kt deleted file mode 100644 index 84fc236ad..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabaseHelper.kt +++ /dev/null @@ -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 - @Deprecated("") - get() { - val set = HashSet(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) { - 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" - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubRawApiServices.kt b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt similarity index 72% rename from app/src/main/java/com/topjohnwu/magisk/data/network/GithubRawApiServices.kt rename to app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt index f90468678..8ed29d5ad 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubRawApiServices.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt @@ -2,15 +2,14 @@ package com.topjohnwu.magisk.data.network import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.model.entity.UpdateInfo +import com.topjohnwu.magisk.tasks.GithubRepoInfo +import io.reactivex.Flowable import io.reactivex.Single import okhttp3.ResponseBody -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Streaming -import retrofit2.http.Url +import retrofit2.adapter.rxjava2.Result +import retrofit2.http.* - -interface GithubRawApiServices { +interface GithubRawServices { //region topjohnwu/magisk_files @@ -41,6 +40,9 @@ interface GithubRawApiServices { @Streaming fun fetchInstaller(): Single + @GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}") + fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single + //endregion /** @@ -51,6 +53,9 @@ interface GithubRawApiServices { @Streaming fun fetchFile(@Url url: String): Single + @GET + fun fetchString(@Url url: String): Single + companion object { private const val REVISION = "revision" @@ -63,4 +68,14 @@ interface GithubRawApiServices { private const val MAGISK_MODULES = "Magisk-Modules-Repo" } +} + +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>> + } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/FileRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/FileRepository.kt deleted file mode 100644 index e1d9c7f1f..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/FileRepository.kt +++ /dev/null @@ -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() - -} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt index bfdf9ffe2..f61d83715 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt @@ -1,12 +1,11 @@ package com.topjohnwu.magisk.data.repository -import android.content.Context import android.content.pm.PackageManager import com.topjohnwu.magisk.App import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Info 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.HideTarget import com.topjohnwu.magisk.utils.Utils @@ -16,9 +15,8 @@ import com.topjohnwu.superuser.Shell import io.reactivex.Single class MagiskRepository( - private val context: Context, - private val apiRaw: GithubRawApiServices, - private val packageManager: PackageManager + private val apiRaw: GithubRawServices, + private val packageManager: PackageManager ) { fun fetchSafetynet() = apiRaw.fetchSafetynet() diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt new file mode 100644 index 000000000..9395a06ea --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt @@ -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") +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt index f9015063c..0ccaef70b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt @@ -1,7 +1,9 @@ package com.topjohnwu.magisk.di +import android.content.Context +import androidx.room.Room import com.topjohnwu.magisk.data.database.* -import com.topjohnwu.magisk.tasks.UpdateRepos +import com.topjohnwu.magisk.tasks.RepoUpdater import org.koin.dsl.module @@ -10,6 +12,13 @@ val databaseModule = module { single { PolicyDao(get()) } single { SettingsDao() } single { StringDao() } - single { RepoDatabaseHelper(get()) } - single { UpdateRepos(get()) } + single { createRepoDatabase(get()) } + single { get().repoDao() } + single { RepoUpdater(get(), get()) } } + +fun createRepoDatabase(context: Context) = + Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db") + .fallbackToDestructiveMigration() + .allowMainThreadQueries() + .build() diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt index 9601778a1..f47f0aaca 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt @@ -4,20 +4,23 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.topjohnwu.magisk.BuildConfig 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.logging.HttpLoggingInterceptor import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory import se.ansman.kotshi.KotshiJsonAdapterFactory val networkingModule = module { single { createOkHttpClient() } single { createMoshiConverterFactory() } single { createRetrofit(get(), get()) } - single { createApiService(get(), Const.Url.GITHUB_RAW_API_URL) } + single { createApiService(get(), Const.Url.GITHUB_RAW_URL) } + single { createApiService(get(), Const.Url.GITHUB_API_URL) } } fun createOkHttpClient(): OkHttpClient { @@ -45,6 +48,7 @@ fun createRetrofit( converterFactory: MoshiConverterFactory ): Retrofit.Builder { return Retrofit.Builder() + .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(converterFactory) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(okHttpClient) diff --git a/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt index 6b62c6d01..e1cad742f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt @@ -1,15 +1,15 @@ package com.topjohnwu.magisk.di 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.MagiskRepository +import com.topjohnwu.magisk.data.repository.StringRepository import org.koin.dsl.module val repositoryModule = module { - single { MagiskRepository(get(), get(), get()) } + single { MagiskRepository(get(), get()) } single { LogRepository(get()) } single { AppRepository(get()) } - single { FileRepository(get()) } + single { StringRepository(get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt index 5d66f84b3..e0309e837 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt @@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.Config 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.Magisk import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module @@ -24,10 +24,10 @@ import java.io.InputStream abstract class RemoteFileService : NotificationService() { - private val repo by inject() + private val service: GithubRawServices by inject() private val supportedFolders - get() = listOfNotNull( + get() = listOf( cacheDir, 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()) } .flatMap { stream -> when (subject) { - is Module -> repo.downloadInstaller() + is Module -> service.fetchInstaller() .map { stream.toModule(subject.file, it.byteStream()); subject.file } else -> Single.fromCallable { stream.writeTo(subject.file); subject.file } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/OldBaseModule.java b/app/src/main/java/com/topjohnwu/magisk/model/entity/OldBaseModule.java deleted file mode 100644 index a8556e65c..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/OldBaseModule.java +++ /dev/null @@ -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, 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 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; - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/Repo.java b/app/src/main/java/com/topjohnwu/magisk/model/entity/Repo.java deleted file mode 100644 index e16b0ef26..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/Repo.java +++ /dev/null @@ -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 CREATOR = new Parcelable.Creator() { - - @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); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt index 44f420414..aaa1ad6e9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt @@ -5,7 +5,7 @@ import android.os.Parcelable import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Info 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.get import kotlinx.android.parcel.IgnoredOnParcel @@ -20,8 +20,8 @@ sealed class DownloadSubject : Parcelable { @Parcelize data class Module( - val module: Repo, - val configuration: Configuration + val module: Repo, + val configuration: Configuration ) : DownloadSubject() { override val url: String get() = module.zipUrl diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt new file mode 100644 index 000000000..55004495a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt @@ -0,0 +1,41 @@ +package com.topjohnwu.magisk.model.entity.module + +abstract class BaseModule : Comparable { + 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) { + 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) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskModule.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt similarity index 61% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskModule.kt rename to app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt index 5846388e9..8b7d5fa3b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt @@ -1,50 +1,10 @@ -package com.topjohnwu.magisk.model.entity +package com.topjohnwu.magisk.model.entity.module import androidx.annotation.WorkerThread import com.topjohnwu.magisk.Const import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile -abstract class BaseModule : Comparable { - 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) { - 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() { override var id: String = "" override var name: String = "" diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt new file mode 100644 index 000000000..88f478989 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt index 2b49c6672..71c2d8ec8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -6,8 +6,8 @@ import com.skoumal.teanity.databinding.ComparableRvItem import com.skoumal.teanity.extensions.addOnPropertyChangedCallback import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.model.entity.Module -import com.topjohnwu.magisk.model.entity.Repo +import com.topjohnwu.magisk.model.entity.module.Module +import com.topjohnwu.magisk.model.entity.module.Repo import com.topjohnwu.magisk.utils.get import com.topjohnwu.magisk.utils.toggle @@ -69,11 +69,7 @@ class RepoRvItem(val item: Repo) : ComparableRvItem() { override val layoutRes: Int = R.layout.item_repo - override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version - && item.lastUpdate == other.item.lastUpdate - && item.versionCode == other.item.versionCode - && item.description == other.item.description - && item.detailUrl == other.item.detailUrl + override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index a47adff93..bec22b81f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -3,7 +3,7 @@ package com.topjohnwu.magisk.model.events import android.app.Activity import com.skoumal.teanity.viewevents.ViewEvent 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt b/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt new file mode 100644 index 000000000..896bb13c3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt @@ -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 + + 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) = 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 = + 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 { + 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java b/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java deleted file mode 100644 index a4a9bd953..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java +++ /dev/null @@ -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 cached; - private Queue> 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 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 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 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 exec() { - return exec(false); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 74ba92c45..0766b5bed 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -9,18 +9,16 @@ import com.skoumal.teanity.util.DiffObservableList import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.data.database.RepoDatabaseHelper -import com.topjohnwu.magisk.model.entity.Module -import com.topjohnwu.magisk.model.entity.Repo +import com.topjohnwu.magisk.data.database.RepoDao +import com.topjohnwu.magisk.model.entity.module.Module import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem import com.topjohnwu.magisk.model.events.InstallModuleEvent import com.topjohnwu.magisk.model.events.OpenChangelogEvent 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.utils.toList import com.topjohnwu.magisk.utils.toSingle import com.topjohnwu.magisk.utils.update import io.reactivex.Single @@ -28,9 +26,9 @@ import io.reactivex.disposables.Disposable import me.tatarka.bindingcollectionadapter2.OnItemBind class ModuleViewModel( - private val resources: Resources, - private val repoDatabase: RepoDatabaseHelper, - private val repoUpdater: UpdateRepos + private val resources: Resources, + private val repoUpdater: RepoUpdater, + private val repoDB: RepoDao ) : MagiskViewModel() { val query = KObservableField("") @@ -65,9 +63,10 @@ class ModuleViewModel( .toList() .map { it to itemsInstalled.calculateDiff(it) } .doOnSuccessUi { itemsInstalled.update(it.first, it.second) } - .flatMap { repoUpdater.exec(force) } - .flatMap { Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } } } - .flattenAsFlowable { it } + .toFlowable() + .flatMap { repoUpdater(force) } + .collect({}, {_, _ -> }) + .flattenAsFlowable { repoDB.repos } .map { RepoRvItem(it) } .toList() .doOnSuccess { allItems.update(it) } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt index 191122be7..f2f4b1839 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt @@ -11,7 +11,7 @@ import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.R import com.topjohnwu.magisk.databinding.FragmentReposBinding 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.DownloadSubject import com.topjohnwu.magisk.model.events.InstallModuleEvent @@ -89,7 +89,7 @@ class ReposFragment : MagiskFragment(), } private fun openChangelog(item: Repo) { - MarkDownWindow.show(requireActivity(), null, item.detailUrl) + MarkDownWindow.show(requireActivity(), null, item.readme) } @SuppressLint("MissingPermission") diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt index 71564b1da..178c822f9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt @@ -8,7 +8,6 @@ import android.view.LayoutInflater import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.core.content.edit import androidx.databinding.DataBindingUtil import androidx.preference.ListPreference import androidx.preference.Preference @@ -20,7 +19,7 @@ import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Const 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.model.observer.Observer import com.topjohnwu.magisk.ui.base.BasePreferenceFragment @@ -33,7 +32,7 @@ import java.io.File class SettingsFragment : BasePreferenceFragment() { - private val repoDatabase: RepoDatabaseHelper by inject() + private val repoDB: RepoDao by inject() private lateinit var updateChannel: ListPreference private lateinit var autoRes: ListPreference @@ -76,10 +75,7 @@ class SettingsFragment : BasePreferenceFragment() { true } findPreference("clear").setOnPreferenceClickListener { - prefs.edit { - remove(Config.Key.ETAG_KEY) - } - repoDatabase.clearRepo() + repoDB.clear() Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT) true } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt index 6f5de3ab1..9f8f17324 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt @@ -4,9 +4,11 @@ import android.content.Context import android.view.LayoutInflater import android.widget.TextView import androidx.appcompat.app.AlertDialog +import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.R -import com.topjohnwu.net.Networking -import com.topjohnwu.net.ResponseListener +import com.topjohnwu.magisk.data.repository.StringRepository +import com.topjohnwu.magisk.utils.inject +import io.reactivex.Single import ru.noties.markwon.Markwon import ru.noties.markwon.html.HtmlPlugin import ru.noties.markwon.image.ImagesPlugin @@ -16,20 +18,22 @@ import java.util.* object MarkDownWindow { + private val stringRepo: StringRepository by inject() + 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) { - Scanner(input, "UTF-8").use { - it.useDelimiter("\\A") - Listener(activity, title).onResponse(it.next()) - } + Single.just(Scanner(input, "UTF-8").apply { useDelimiter("\\A") }) + .map { it.next() } + .also { + show(activity, title, it) + } } - internal class Listener(var activity: Context, var title: String?) : ResponseListener { - - override fun onResponse(md: String) { + fun show(activity: Context, title: String?, content: Single) { + content.subscribeK { val markwon = Markwon.builder(activity) .usePlugin(HtmlPlugin.create()) .usePlugin(ImagesPlugin.create(activity)) @@ -40,7 +44,7 @@ object MarkDownWindow { val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null) val tv = mv.findViewById(R.id.md_txt) try { - markwon.setMarkdown(tv, md) + markwon.setMarkdown(tv, it) } catch (e: ExceptionInInitializerError) { //Nothing we can do about this error other than show error message tv.setText(R.string.download_file_error) diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml index cc0ec5035..fe15c4803 100644 --- a/app/src/main/res/layout/item_repo.xml +++ b/app/src/main/res/layout/item_repo.xml @@ -47,7 +47,7 @@ android:id="@+id/version_name" android:layout_width="0dp" 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:textColor="@android:color/tertiary_text_dark" android:textIsSelectable="false" @@ -62,7 +62,7 @@ android:id="@+id/author" android:layout_width="0dp" 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:textColor="@android:color/tertiary_text_dark" android:textIsSelectable="false" @@ -78,7 +78,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" 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:textIsSelectable="false" app:layout_constraintBottom_toTopOf="@+id/update_time"