Added (ported back) features from initial design [retrofit,moshi,kotpref]

Marked most of the old classes using Networking as deprecated to clearly visualise their future removal
This commit is contained in:
Viktor De Pasquale 2019-05-06 19:03:28 +02:00
parent 5d632d0d90
commit b018124226
39 changed files with 1374 additions and 19 deletions

View File

@ -47,6 +47,10 @@ android {
}
}
androidExtensions {
experimental = true
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':net')
@ -72,8 +76,23 @@ dependencies {
implementation "org.koin:koin-android:${koin}"
implementation "org.koin:koin-androidx-viewmodel:${koin}"
def vRetrofit = "2.5.0"
def vOkHttp = "3.12.0"
def vMoshi = "1.8.0"
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
implementation "com.squareup.moshi:moshi:${vMoshi}"
implementation "com.squareup.moshi:moshi-kotlin:${vMoshi}"
def vKotpref = "2.8.0"
implementation "com.chibatching.kotpref:kotpref:${vKotpref}"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha04'
implementation 'androidx.cardview:cardview:1.0.0'

View File

@ -0,0 +1,20 @@
package com.topjohnwu.magisk
import android.os.Process
object Constants {
// Paths
val MAGISK_PATH = "/sbin/.magisk/img"
val MAGISK_LOG = "/cache/magisk.log"
val USER_ID = Process.myUid() / 100000
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
const val GITHUB_URL = "https://github.com/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
}

View File

@ -0,0 +1,36 @@
package com.topjohnwu.magisk
import androidx.appcompat.app.AppCompatDelegate
import com.chibatching.kotpref.KotprefModel
import com.topjohnwu.magisk.KConfig.UpdateChannel.*
object KConfig : KotprefModel() {
override val kotprefName: String = "${context.packageName}_preferences"
var darkMode by intPref(default = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, key = "darkMode")
var magiskChecksum by stringPref("", "magiskChecksum")
var forceEncrypt by booleanPref(false, "forceEncryption")
var keepVerity by booleanPref(false, "keepVerity")
var bootFormat by stringPref("img", "bootFormat")
var suLogTimeout by longPref(0, "suLogTimeout")
private var internalUpdateChannel by stringPref(
KConfig.UpdateChannel.STABLE.toString(),
"updateChannel"
)
var useCustomTabs by booleanPref(true, "useCustomTabs")
var updateChannel: UpdateChannel
get() = valueOf(internalUpdateChannel)
set(value) {
internalUpdateChannel = value.toString()
}
val isStable get() = !(isCanary || isBeta)
val isCanary get() = updateChannel == CANARY || updateChannel == CANARY_DEBUG
val isBeta get() = updateChannel == BETA
enum class UpdateChannel {
STABLE, BETA, CANARY, CANARY_DEBUG
}
}

View File

@ -0,0 +1,19 @@
package com.topjohnwu.magisk.data.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.topjohnwu.magisk.model.entity.Repository
@Database(
version = 1,
entities = [Repository::class]
)
abstract class AppDatabase : RoomDatabase() {
companion object {
const val NAME = "database"
}
abstract fun repoDao(): RepositoryDao
}

View File

@ -0,0 +1,34 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.model.entity.toMap
import java.util.concurrent.TimeUnit
class LogDao : BaseDao() {
override val table = DatabaseDefinition.Table.LOG
fun deleteOutdated(
suTimeout: Long = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1)
) = query<Delete> {
condition {
lessThan("time", suTimeout.toString())
}
}.ignoreElement()
fun deleteAll() = query<Delete> {}.ignoreElement()
fun fetchAll() = query<Select> {
orderBy("time", Order.DESC)
}.flattenAsFlowable { it }
.map { it.toLog() }
.toList()
fun put(log: MagiskLog) = query<Insert> {
values(log.toMap())
}.ignoreElement()
}

View File

@ -20,6 +20,7 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
@Deprecated
public class MagiskDB {
private static final String POLICY_TABLE = "policies";
@ -27,20 +28,24 @@ public class MagiskDB {
private static final String SETTINGS_TABLE = "settings";
private static final String STRINGS_TABLE = "strings";
private PackageManager pm;
private final PackageManager pm;
@Deprecated
public MagiskDB(Context context) {
pm = context.getPackageManager();
}
@Deprecated
public void deletePolicy(Policy policy) {
deletePolicy(policy.uid);
}
@Deprecated
private List<String> rawSQL(String fmt, Object... args) {
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
}
@Deprecated
private List<ContentValues> SQL(String fmt, Object... args) {
List<ContentValues> list = new ArrayList<>();
for (String raw : rawSQL(fmt, args)) {
@ -57,6 +62,7 @@ public class MagiskDB {
return list;
}
@Deprecated
private String toSQL(ContentValues values) {
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
keys.append('(');
@ -80,6 +86,7 @@ public class MagiskDB {
return keys.toString();
}
@Deprecated
public void clearOutdated() {
rawSQL(
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
@ -89,14 +96,17 @@ public class MagiskDB {
);
}
@Deprecated
public void deletePolicy(String pkg) {
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
}
@Deprecated
public void deletePolicy(int uid) {
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
}
@Deprecated
public Policy getPolicy(int uid) {
List<ContentValues> res =
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
@ -110,10 +120,12 @@ public class MagiskDB {
return null;
}
@Deprecated
public void updatePolicy(Policy policy) {
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
}
@Deprecated
public List<Policy> getPolicyList() {
List<Policy> list = new ArrayList<>();
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
@ -127,6 +139,7 @@ public class MagiskDB {
return list;
}
@Deprecated
public List<List<SuLogEntry>> getLogs() {
List<List<SuLogEntry>> ret = new ArrayList<>();
List<SuLogEntry> list = null;
@ -144,18 +157,22 @@ public class MagiskDB {
return ret;
}
@Deprecated
public void addLog(SuLogEntry log) {
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
}
@Deprecated
public void clearLogs() {
rawSQL("DELETE FROM %s", LOG_TABLE);
}
@Deprecated
public void rmSettings(String key) {
rawSQL("DELETE FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
}
@Deprecated
public void setSettings(String key, int value) {
ContentValues data = new ContentValues();
data.put("key", key);
@ -163,6 +180,7 @@ public class MagiskDB {
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
}
@Deprecated
public int getSettings(String key, int defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
if (res.isEmpty())
@ -170,6 +188,7 @@ public class MagiskDB {
return res.get(0).getAsInteger("value");
}
@Deprecated
public void setStrings(String key, String value) {
if (value == null) {
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
@ -181,6 +200,7 @@ public class MagiskDB {
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
}
@Deprecated
public String getStrings(String key, String defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
if (res.isEmpty())

View File

@ -0,0 +1,67 @@
package com.topjohnwu.magisk.data.database
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.utils.now
import java.util.concurrent.TimeUnit
class PolicyDao(
private val context: Context
) : BaseDao() {
override val table: String = DatabaseDefinition.Table.POLICY
fun deleteOutdated(
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
) = query<Delete> {
condition {
greaterThan("until", "0")
and {
lessThan("until", nowSeconds.toString())
}
}
}.ignoreElement()
fun delete(packageName: String) = query<Delete> {
condition {
equals("package_name", packageName)
}
}.ignoreElement()
fun delete(uid: Int) = query<Delete> {
condition {
equals("uid", uid.toString())
}
}.ignoreElement()
fun fetch(uid: Int) = query<Select> {
condition {
equals("uid", uid.toString())
}
}.map { it.first().toPolicy(context.packageManager) }
.doOnError {
if (it is PackageManager.NameNotFoundException) {
delete(uid).subscribe()
}
}
fun update(policy: MagiskPolicy) = query<Replace> {
values(policy.toMap())
}.ignoreElement()
fun fetchAll() = query<Select> {
condition {
equals("uid/100000", Constants.USER_ID.toString())
}
}.flattenAsFlowable { it }
.map { it.toPolicy(context.packageManager) }
.toList()
}

View File

@ -11,13 +11,15 @@ import com.topjohnwu.magisk.model.entity.Repo;
import java.util.HashSet;
import java.util.Set;
@Deprecated
public class RepoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 5;
private static final String TABLE_NAME = "repos";
private SQLiteDatabase mDb;
private final SQLiteDatabase mDb;
@Deprecated
public RepoDatabaseHelper(Context context) {
super(context, "repo.db", null, DATABASE_VER);
mDb = getWritableDatabase();
@ -46,19 +48,23 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
onUpgrade(db, 0, DATABASE_VER);
}
@Deprecated
public void clearRepo() {
mDb.delete(TABLE_NAME, null, null);
}
@Deprecated
public void removeRepo(String id) {
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
}
@Deprecated
public void removeRepo(Repo repo) {
removeRepo(repo.getId());
}
@Deprecated
public void removeRepo(Iterable<String> list) {
for (String id : list) {
if (id == null) continue;
@ -66,10 +72,12 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
}
}
@Deprecated
public void addRepo(Repo repo) {
mDb.replace(TABLE_NAME, null, repo.getContentValues());
}
@Deprecated
public Repo getRepo(String id) {
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
if (c.moveToNext()) {
@ -79,10 +87,12 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
return null;
}
@Deprecated
public Cursor getRawCursor() {
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
}
@Deprecated
public Cursor getRepoCursor() {
String orderBy = null;
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
@ -95,6 +105,7 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
}
@Deprecated
public Set<String> getRepoIDSet() {
HashSet<String> set = new HashSet<>(300);
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {

View File

@ -0,0 +1,17 @@
package com.topjohnwu.magisk.data.database
import androidx.room.Dao
import androidx.room.Query
import com.skoumal.teanity.database.BaseDao
import com.topjohnwu.magisk.model.entity.Repository
@Dao
interface RepositoryDao : BaseDao<Repository> {
@Query("DELETE FROM repos")
override fun deleteAll()
@Query("SELECT * FROM repos")
override fun fetchAll(): List<Repository>
}

View File

@ -0,0 +1,21 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
class SettingsDao : BaseDao() {
override val table = DatabaseDefinition.Table.SETTINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: Int) = query<Insert> {
values(key to value.toString())
}.ignoreElement()
fun fetch(key: String) = query<Select> {
condition { equals("key", key) }
}.map { it.first().values.first().toIntOrNull() ?: -1 }
}

View File

@ -0,0 +1,22 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
class StringsDao : BaseDao() {
override val table = DatabaseDefinition.Table.STRINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: String) = query<Insert> {
values(key to value)
}.ignoreElement()
fun fetch(key: String, default: String = "") = query<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
}

View File

@ -0,0 +1,15 @@
package com.topjohnwu.magisk.data.database.base
abstract class BaseDao {
abstract val table: String
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { MagiskQuery(it) }
.query()
}

View File

@ -0,0 +1,33 @@
package com.topjohnwu.magisk.data.database.base
import androidx.annotation.AnyThread
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
object DatabaseDefinition {
object Table {
const val POLICY = "policies"
const val LOG = "logs"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
}
@AnyThread
fun MagiskQuery.query() = query.su()
fun String.suRaw() = Single.just(Shell.su(this))
.map { it.exec().out }
fun String.su() = suRaw()
.map { it.toMap() }
fun List<String>.toMap() = map { it.split(Regex("\\|")) }
.map { it.toMapInternal() }
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()

View File

@ -0,0 +1,5 @@
package com.topjohnwu.magisk.data.database.base
data class MagiskQuery(private val _query: String) {
val query = "magisk --sqlite $_query"
}

View File

@ -0,0 +1,157 @@
package com.topjohnwu.magisk.data.database.base
import androidx.annotation.StringDef
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
interface MagiskQueryBuilder {
val requestType: String
var table: String
companion object {
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
Builder::class.java.newInstance()
.apply(builder)
.toString()
.let { MagiskQuery(it) }
}
}
class Delete : MagiskQueryBuilder {
override val requestType: String = "DELETE FROM"
override var table = ""
private var condition = ""
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln(condition)
.toString()
}
}
class Select : MagiskQueryBuilder {
override val requestType: String get() = "SELECT $fields FROM"
override lateinit var table: String
private var fields = "*"
private var condition = ""
private var orderField = ""
fun fields(vararg newFields: String) {
if (newFields.isEmpty()) {
fields = "*"
return
}
fields = newFields.joinToString(", ")
}
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
fun orderBy(field: String, @OrderStrict order: String) {
orderField = "ORDER BY $field $order"
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln(condition)
.appendln(orderField)
.toString()
}
}
class Replace : Insert() {
override val requestType: String = "REPLACE INTO"
}
open class Insert : MagiskQueryBuilder {
override val requestType: String = "INSERT INTO"
override lateinit var table: String
private val keys get() = _values.keys.joinToString(",")
private val values get() = _values.values.joinToString(",")
private var _values: Map<String, String> = mapOf()
fun values(vararg pairs: Pair<String, String>) {
_values = pairs.toMap()
}
fun values(values: Map<String, String>) {
_values = values
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln("($keys) VALUES($values)")
.toString()
}
}
class Condition {
private val conditionWord = "WHERE %s"
private var condition: String = ""
fun equals(field: String, value: String) {
condition = "$field=\"$value\""
}
fun greaterThan(field: String, value: String) {
condition = "$field > $value"
}
fun lessThan(field: String, value: String) {
condition = "$field < $value"
}
fun greaterOrEqualTo(field: String, value: String) {
condition = "$field >= $value"
}
fun lessOrEqualTo(field: String, value: String) {
condition = "$field <= $value"
}
fun and(builder: Condition.() -> Unit) {
condition += " " + Condition().apply(builder).condition
}
fun or(builder: Condition.() -> Unit) {
condition += " " + Condition().apply(builder).condition
}
override fun toString(): String {
return conditionWord.format(condition)
}
}
class Order {
@set:OrderStrict
var order = DESC
var field = ""
companion object {
const val ASC = "ASC"
const val DESC = "DESC"
}
}
@StringDef(ASC, DESC)
@Retention(AnnotationRetention.SOURCE)
annotation class OrderStrict

View File

@ -0,0 +1,22 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.model.entity.GithubRepo
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.Query
interface GithubApiServices {
@GET("users/Magisk-Modules-Repo/repos")
fun fetchRepos(
@Query("page") page: Int,
@Query("per_page") count: Int = REPOS_PER_PAGE,
@Query("sort") sortOrder: String = "pushed"
): Single<List<GithubRepo>>
companion object {
const val REPOS_PER_PAGE = 100
}
}

View File

@ -0,0 +1,75 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.model.entity.MagiskConfig
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
interface GithubRawApiServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
fun fetchCanaryConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming
fun fetchBootctl(@Path(REVISION) revision: String = Constants.BOOTCTL_REVISION): Single<ResponseBody>
//endregion
//region topjohnwu/Magisk/master
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
@Streaming
fun fetchModuleInstaller(): Single<ResponseBody>
//endregion
//region Magisk-Modules-Repo
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
@Streaming
fun fetchFile(id: String, file: String): Single<ResponseBody>
//endregion
/**
* This method shall be used exclusively for fetching files from urls from previous requests.
* Him, who uses it in a wrong way, shall die in an eternal flame.
* */
@GET
@Streaming
fun fetchFile(@Url url: String): Single<ResponseBody>
companion object {
private const val REVISION = "revision"
private const val MODULE = "module"
private const val FILE = "file"
private const val MAGISK_FILES = "topjohnwu/magisk_files"
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
}
}

View File

@ -0,0 +1,21 @@
package com.topjohnwu.magisk.data.network
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Streaming
interface GithubServices {
@GET("Magisk-Modules-Repo/{$MODULE}/archive/master.zip")
@Streaming
fun fetchModuleZip(@Path(MODULE) module: String): Single<ResponseBody>
companion object {
private const val MODULE = "module"
}
}

View File

@ -0,0 +1,45 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.LogDao
import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import timber.log.Timber
import java.util.concurrent.TimeUnit
class LogRepository(
private val logDao: LogDao
) {
fun fetchLogs() = logDao.fetchAll()
.map { it.sortByDescending { it.date.time }; it }
.map { it.wrap() }
fun fetchMagiskLogs() = "tail -n 5000 ${Constants.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() }
.map { Timber.i(it.toString()); it }
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
val day = TimeUnit.DAYS.toMillis(1)
var currentDay = firstOrNull()?.date?.time ?: return listOf()
var tempList = this
val outList = mutableListOf<WrappedMagiskLog>()
while (tempList.isNotEmpty()) {
val logsGivenDay = takeWhile { it.date.time / day == currentDay / day }
currentDay = tempList.firstOrNull()?.date?.time ?: currentDay + day
if (logsGivenDay.isEmpty())
continue
outList.add(WrappedMagiskLog(currentDay / day * day, logsGivenDay))
tempList = tempList.subList(logsGivenDay.size, tempList.size)
}
return outList
}
}

View File

@ -0,0 +1,78 @@
package com.topjohnwu.magisk.data.repository
import android.content.Context
import com.topjohnwu.magisk.KConfig
import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.model.entity.Version
import com.topjohnwu.magisk.utils.writeToFile
import io.reactivex.Single
import io.reactivex.functions.BiFunction
class MagiskRepository(
private val context: Context,
private val apiRaw: GithubRawApiServices
) {
private val config = apiRaw.fetchConfig()
private val configBeta = apiRaw.fetchBetaConfig()
private val configCanary = apiRaw.fetchCanaryConfig()
private val configCanaryDebug = apiRaw.fetchCanaryDebugConfig()
fun fetchMagisk() = fetchConfig()
.flatMap { apiRaw.fetchFile(it.magisk.link) }
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
fun fetchManager() = fetchConfig()
.flatMap { apiRaw.fetchFile(it.app.link) }
.map { it.writeToFile(context, FILE_MAGISK_APK) }
fun fetchUninstaller() = fetchConfig()
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
fun fetchSafetynet() = apiRaw
.fetchSafetynet()
.map { it.writeToFile(context, FILE_SAFETY_NET_APK) }
fun fetchBootctl() = apiRaw
.fetchBootctl()
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
fun fetchConfig() = when (KConfig.updateChannel) {
KConfig.UpdateChannel.STABLE -> config
KConfig.UpdateChannel.BETA -> configBeta
KConfig.UpdateChannel.CANARY -> configCanary
KConfig.UpdateChannel.CANARY_DEBUG -> configCanaryDebug
}
fun fetchMagiskVersion(): Single<Version> = Single.zip(
fetchMagiskVersionName(),
fetchMagiskVersionCode(),
BiFunction { versionName, versionCode ->
Version(versionName, versionCode)
}
)
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
.map { it.first() }
.map { it.substring(0 until it.indexOf(":")) }
.onErrorReturn { "Unknown" }
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
.map { it.first() }
.map { it.toIntOrNull() ?: -1 }
.onErrorReturn { -1 }
companion object {
const val FILE_MAGISK_ZIP = "magisk.zip"
const val FILE_MAGISK_APK = "magisk.apk"
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
const val FILE_SAFETY_NET_APK = "safetynet.apk"
const val FILE_BOOTCTL_SH = "bootctl"
}
}

View File

@ -0,0 +1,67 @@
package com.topjohnwu.magisk.data.repository
import android.content.Context
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.data.network.GithubServices
import com.topjohnwu.magisk.model.entity.GithubRepo
import com.topjohnwu.magisk.model.entity.toRepository
import com.topjohnwu.magisk.utils.writeToFile
import com.topjohnwu.magisk.utils.writeToString
import io.reactivex.Single
class ModuleRepository(
private val context: Context,
private val apiRaw: GithubRawApiServices,
private val api: GithubApiServices,
private val apiWeb: GithubServices
) {
fun fetchModules() = fetchAllRepos()
.flattenAsFlowable { it }
.flatMapSingle { fetchProperties(it.name, it.updatedAtMillis) }
.toList()
fun fetchInstallFile(module: String) = apiRaw
.fetchFile(module, FILE_INSTALL_SH)
.map { it.writeToFile(context, FILE_INSTALL_SH) }
fun fetchReadme(module: String) = apiRaw
.fetchFile(module, FILE_README_MD)
.map { it.writeToString() }
fun fetchConfig(module: String) = apiRaw
.fetchFile(module, FILE_CONFIG_SH)
.map { it.writeToFile(context, FILE_CONFIG_SH) }
fun fetchInstallZip(module: String) = apiWeb
.fetchModuleZip(module)
.map { it.writeToFile(context, FILE_INSTALL_ZIP) }
fun fetchInstaller() = apiRaw
.fetchModuleInstaller()
.map { it.writeToFile(context, FILE_MODULE_INSTALLER_SH) }
private fun fetchProperties(module: String, lastChanged: Long) = apiRaw
.fetchFile(module, "module.prop")
.map { it.toRepository(lastChanged) }
private fun fetchAllRepos(page: Int = 0): Single<List<GithubRepo>> = api.fetchRepos(page)
.flatMap {
if (it.size == GithubApiServices.REPOS_PER_PAGE) {
fetchAllRepos(page + 1).map { newList -> it + newList }
} else {
Single.just(it)
}
}
companion object {
const val FILE_INSTALL_SH = "install.sh"
const val FILE_README_MD = "README.md"
const val FILE_CONFIG_SH = "config.sh"
const val FILE_INSTALL_ZIP = "install.zip"
const val FILE_MODULE_INSTALLER_SH = "module_installer.sh"
}
}

View File

@ -1,6 +1,65 @@
package com.topjohnwu.magisk.di
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.data.network.GithubServices
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import retrofit2.CallAdapter
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
val networkingModule = module {
single { createOkHttpClient() }
val networkingModule = module {}
single { createConverterFactory() }
single { createCallAdapterFactory() }
single { createRetrofit(get(), get(), get()) }
single { createApiService<GithubServices>(get(), Constants.GITHUB_URL) }
single { createApiService<GithubApiServices>(get(), Constants.GITHUB_API_URL) }
single { createApiService<GithubRawApiServices>(get(), Constants.GITHUB_RAW_API_URL) }
}
fun createOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
}
fun createConverterFactory(): Converter.Factory {
val moshi = Moshi.Builder().build()
return MoshiConverterFactory.create(moshi)
}
fun createCallAdapterFactory(): CallAdapter.Factory {
return RxJava2CallAdapterFactory.create()
}
fun createRetrofit(
okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
callAdapterFactory: CallAdapter.Factory
): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(converterFactory)
.addCallAdapterFactory(callAdapterFactory)
.client(okHttpClient)
}
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
return retrofitBuilder
.baseUrl(baseUrl)
.build()
.create(T::class.java)
}

View File

@ -0,0 +1,12 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import com.topjohnwu.magisk.utils.timeFormatStandard
import com.topjohnwu.magisk.utils.toTime
data class GithubRepo(
@Json(name = "name") val name: String,
@Json(name = "updated_at") val updatedAt: String
) {
val updatedAtMillis by lazy { updatedAt.toTime(timeFormatStandard) }
}

View File

@ -0,0 +1,32 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
data class MagiskConfig(
val app: MagiskApp,
val uninstaller: MagiskLink,
val magisk: MagiskFlashable
)
@JsonClass(generateAdapter = true)
data class MagiskApp(
val version: String,
val versionCode: String,
val link: String,
val note: String
)
@JsonClass(generateAdapter = true)
data class MagiskLink(
val link: String
)
@JsonClass(generateAdapter = true)
data class MagiskFlashable(
val version: String,
val versionCode: String,
val link: String,
val note: String,
@Json(name = "md5") val hash: String
)

View File

@ -0,0 +1,46 @@
package com.topjohnwu.magisk.model.entity
import java.util.*
data class MagiskLog(
val fromUid: Int,
val toUid: Int,
val fromPid: Int,
val packageName: String,
val appName: String,
val command: String,
val action: Boolean,
val date: Date
)
data class WrappedMagiskLog(
val time: Long,
val items: List<MagiskLog>
)
fun Map<String, String>.toLog(): MagiskLog {
return MagiskLog(
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
toUid = get("to_uid")?.toIntOrNull() ?: -1,
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
packageName = get("package_name").orEmpty(),
appName = get("app_name").orEmpty(),
command = get("command").orEmpty(),
action = get("action")?.toIntOrNull() != 0,
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
)
}
fun Long.toDate() = Date(this)
fun MagiskLog.toMap() = mapOf(
"from_uid" to fromUid,
"to_uid" to toUid,
"from_pid" to fromPid,
"package_name" to packageName,
"app_name" to appName,
"command" to command,
"action" to action,
"time" to date
).mapValues { it.toString() }

View File

@ -0,0 +1,81 @@
package com.topjohnwu.magisk.model.entity
import android.os.Parcelable
import androidx.annotation.AnyThread
import androidx.annotation.NonNull
import androidx.annotation.WorkerThread
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.base.su
import io.reactivex.Single
import kotlinx.android.parcel.Parcelize
import okhttp3.ResponseBody
import java.io.File
interface MagiskModule : Parcelable {
val id: String
val name: String
val author: String
val version: String
val versionCode: String
}
@Entity(tableName = "repos")
@Parcelize
data class Repository(
@PrimaryKey @NonNull
override val id: String,
override val name: String,
override val author: String,
override val version: String,
override val versionCode: String,
val lastUpdate: Long
) : MagiskModule
@Parcelize
data class Module(
override val id: String,
override val name: String,
override val author: String,
override val version: String,
override val versionCode: String,
val path: String
) : MagiskModule
@AnyThread
fun File.toModule(): Single<Module> {
val path = "${Constants.MAGISK_PATH}/$name"
return "dos2unix < $path/module.prop".su()
.map { it.first().toModule(path) }
}
fun Map<String, String>.toModule(path: String): Module {
return Module(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
path = path
)
}
@WorkerThread
fun ResponseBody.toRepository(lastUpdate: Long) = string()
.split(Regex("\\n"))
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()
.toRepository(lastUpdate)
@AnyThread
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
lastUpdate = lastUpdate
)

View File

@ -0,0 +1,74 @@
package com.topjohnwu.magisk.model.entity
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
data class MagiskPolicy(
val uid: Int,
val packageName: String,
val appName: String,
val policy: Int,
val until: Long,
val logging: Boolean,
val notification: Boolean,
val applicationInfo: ApplicationInfo
)
/*@Throws(PackageManager.NameNotFoundException::class)
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = getAsInteger("uid")
val packageName = getAsString("package_name")
val info = pm.getApplicationInfo(packageName, 0)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return MagiskPolicy(
uid = uid,
packageName = packageName,
policy = getAsInteger("policy"),
until = getAsInteger("until").toLong(),
logging = getAsInteger("logging") != 0,
notification = getAsInteger("notification") != 0,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}
fun MagiskPolicy.toContentValues() = ContentValues().apply {
put("uid", uid)
put("uid", uid)
put("package_name", packageName)
put("policy", policy)
put("until", until)
put("logging", if (logging) 1 else 0)
put("notification", if (notification) 1 else 0)
}*/
fun MagiskPolicy.toMap() = mapOf(
"uid" to uid,
"package_name" to packageName,
"policy" to policy,
"until" to until,
"logging" to if (logging) 1 else 0,
"notification" to if (notification) 1 else 0
).mapValues { it.toString() }
@Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = get("uid")?.toIntOrNull() ?: -1
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, 0)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return MagiskPolicy(
uid = uid,
packageName = packageName,
policy = get("policy")?.toIntOrNull() ?: -1,
until = get("until")?.toLongOrNull() ?: -1L,
logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}

View File

@ -0,0 +1,3 @@
package com.topjohnwu.magisk.model.entity
data class Version(val version: String, val versionCode: Int)

View File

@ -0,0 +1,75 @@
package com.topjohnwu.magisk.model.zip
import com.topjohnwu.magisk.utils.forEach
import com.topjohnwu.magisk.utils.withStreams
import com.topjohnwu.superuser.io.SuFile
import java.io.File
import java.util.zip.ZipInputStream
class Zip private constructor(private val values: Builder) {
companion object {
operator fun invoke(builder: Builder.() -> Unit): Zip {
return Zip(Builder().apply(builder))
}
}
class Builder {
lateinit var zip: File
lateinit var destination: File
var excludeDirs = true
}
data class Path(val path: String, val pullFromDir: Boolean = true)
fun unzip(vararg paths: Pair<String, Boolean>) =
unzip(*paths.map { Path(it.first, it.second) }.toTypedArray())
@Suppress("RedundantLambdaArrow")
fun unzip(vararg paths: Path) {
ensureRequiredParams()
values.zip.zipStream().use {
it.forEach { e ->
val currentPath = paths.firstOrNull { e.name.startsWith(it.path) }
val isDirectory = values.excludeDirs && e.isDirectory
if (currentPath == null || isDirectory) {
// Ignore directories, only create files
return@forEach
}
val name = if (currentPath.pullFromDir) {
e.name.substring(e.name.lastIndexOf('/') + 1)
} else {
e.name
}
val out = File(values.destination, name)
.ensureExists()
.outputStream()
//.suOutputStream()
withStreams(it, out) { reader, writer ->
reader.copyTo(writer)
}
}
}
}
private fun ensureRequiredParams() {
if (!values.zip.exists()) {
throw RuntimeException("Zip file does not exist")
}
}
private fun File.ensureExists() =
if ((!parentFile.exists() && !parentFile.mkdirs()) || parentFile is SuFile) {
SuFile(parentFile, name).apply { parentFile.mkdirs() }
} else {
this
}
private fun File.zipStream() = ZipInputStream(inputStream())
}

View File

@ -14,6 +14,7 @@ import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.json.JSONException;
import org.json.JSONObject;
@Deprecated
public class CheckUpdates {
private static Request getRequest() {
@ -56,8 +57,8 @@ public class CheckUpdates {
private static class UpdateListener implements ResponseListener<JSONObject> {
private Runnable cb;
private long start;
private final Runnable cb;
private final long start;
UpdateListener(Runnable callback) {
cb = callback;

View File

@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Deprecated
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;

View File

@ -1,14 +1,15 @@
package com.topjohnwu.magisk.utils;
import androidx.annotation.IntDef;
import androidx.collection.ArraySet;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Set;
import androidx.annotation.IntDef;
import androidx.collection.ArraySet;
@Deprecated
public class Event {
public static final int MAGISK_HIDE_DONE = 0;
@ -23,8 +24,9 @@ public class Event {
public @interface EventID {}
// We will not dynamically add topics, so use arrays instead of hash tables
private static Store[] eventList = new Store[5];
private static final Store[] eventList = new Store[5];
@Deprecated
public static void register(Listener listener, @EventID int... events) {
for (int event : events) {
if (eventList[event] == null)
@ -36,10 +38,12 @@ public class Event {
}
}
@Deprecated
public static void register(AutoListener listener) {
register(listener, listener.getListeningEvents());
}
@Deprecated
public static void unregister(Listener listener, @EventID int... events) {
for (int event : events) {
if (eventList[event] == null)
@ -48,22 +52,27 @@ public class Event {
}
}
@Deprecated
public static void unregister(AutoListener listener) {
unregister(listener, listener.getListeningEvents());
}
@Deprecated
public static void trigger(@EventID int event) {
trigger(true, event, null);
}
@Deprecated
public static void trigger(@EventID int event, Object result) {
trigger(true, event, result);
}
@Deprecated
public static void trigger(boolean perm, @EventID int event) {
trigger(perm, event, null);
}
@Deprecated
public static void trigger(boolean perm, @EventID int event, Object result) {
if (eventList[event] == null)
eventList[event] = new Store();
@ -76,6 +85,7 @@ public class Event {
}
}
@Deprecated
public static void reset(@EventID int event) {
if (eventList[event] == null)
return;
@ -83,17 +93,20 @@ public class Event {
eventList[event].result = null;
}
@Deprecated
public static void reset(AutoListener listener) {
for (int event : listener.getListeningEvents())
reset(event);
}
@Deprecated
public static boolean isTriggered(@EventID int event) {
if (eventList[event] == null)
return false;
return eventList[event].triggered;
}
@Deprecated
public static boolean isTriggered(AutoListener listener) {
for (int event : listener.getListeningEvents()) {
if (!isTriggered(event))
@ -102,22 +115,26 @@ public class Event {
return true;
}
@Deprecated
public static <T> T getResult(@EventID int event) {
return (T) eventList[event].result;
}
@Deprecated
public interface Listener {
void onEvent(int event);
}
@Deprecated
public interface AutoListener extends Listener {
@EventID
int[] getListeningEvents();
}
@Deprecated
private static class Store {
boolean triggered = false;
Set<Listener> listeners = new ArraySet<>();
Object result;
}
public interface Listener {
void onEvent(int event);
}
public interface AutoListener extends Listener {
@EventID
int[] getListeningEvents();
}
}

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.utils
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageInfo
@ -9,6 +10,7 @@ import android.content.pm.PackageManager.*
import android.net.Uri
import android.provider.OpenableColumns
import com.topjohnwu.magisk.App
import java.io.File
import java.io.FileNotFoundException
val PackageInfo.processes
@ -74,3 +76,22 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.readUri(uri: Uri) = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun ApplicationInfo.findAppLabel(pm: PackageManager): String {
return pm.getApplicationLabel(this)?.toString().orEmpty()
}
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun File.provide(): Uri {
val context: Context by inject()
return FileProvider.getUriForFile(context, "com.topjohnwu.magisk.fileprovider", this)
}
fun File.mv(destination: File) {
inputStream().copyTo(destination)
deleteRecursively()
}
fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)

View File

@ -0,0 +1,38 @@
package com.topjohnwu.magisk.utils
import android.net.Uri
import androidx.core.net.toFile
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
var entry: ZipEntry? = nextEntry
while (entry != null) {
callback(entry)
entry = nextEntry
}
}
fun Uri.copyTo(file: File) = toFile().copyTo(file)
fun InputStream.copyTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
fun File.tarInputStream() = TarInputStream(inputStream())
fun File.tarOutputStream() = TarOutputStream(this)
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,
outStream: Out,
withBoth: (In, Out) -> Unit
) {
inStream.use { reader ->
outStream.use { writer ->
withBoth(reader, writer)
}
}
}

View File

@ -0,0 +1,54 @@
package com.topjohnwu.magisk.utils
import android.content.Context
import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import okhttp3.ResponseBody
import java.io.File
fun ResponseBody.writeToFile(context: Context, fileName: String): File {
val file = File(context.cacheDir, fileName)
withStreams(byteStream(), file.outputStream()) { inStream, outStream ->
inStream.copyTo(outStream)
}
return file
}
fun ResponseBody.writeToString() = string()
fun String.launch() = if (Config.useCustomTabs) {
launchWithCustomTabs()
} else {
launchWithIntent()
}
private fun String.launchWithCustomTabs() {
val context: Context by inject()
val primaryColor = ContextCompat.getColor(context, R.color.colorPrimary)
val secondaryColor = ContextCompat.getColor(context, R.color.colorSecondary)
CustomTabsIntent.Builder()
.enableUrlBarHiding()
.setShowTitle(true)
.setToolbarColor(primaryColor)
.setSecondaryToolbarColor(secondaryColor)
.build()
.apply { intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK }
.launchUrl(context, this.toUri())
}
private fun String.launchWithIntent() {
val context: Context by inject()
Intent(Intent.ACTION_VIEW)
.apply {
data = toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
.startActivity(context)
}

View File

@ -0,0 +1,19 @@
package com.topjohnwu.magisk.utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import java.io.File
fun reboot(recovery: Boolean = false): Shell.Result {
val command = StringBuilder("/system/bin/reboot")
.appendIf(recovery) {
append(" recovery")
}
.toString()
return Shell.su(command).exec()
}
fun File.suOutputStream() = SuFileOutputStream(this)
fun File.suInputStream() = SuFileInputStream(this)

View File

@ -8,4 +8,7 @@ fun String.replaceRandomWithSpecial(): String {
random = random()
} while (random == '.')
return replace(random, specialChars.random())
}
}
fun StringBuilder.appendIf(condition: Boolean, builder: StringBuilder.() -> Unit) =
if (condition) apply(builder) else this

View File

@ -0,0 +1,14 @@
package com.topjohnwu.magisk.utils
import android.view.View
import android.view.ViewTreeObserver
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
callback()
}
})

View File

@ -13,6 +13,7 @@ import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
@Deprecated
public class Networking {
private static final int READ_TIMEOUT = 15000;