Modernize Repo class for Magisk modules

- Use Kotlin
- Use Room database
- Use retrofit for networking
- Use RxJava pipeline for repo updating
This commit is contained in:
topjohnwu
2019-07-28 01:54:34 -07:00
parent 0c17ea5755
commit 42e7db8d13
30 changed files with 401 additions and 691 deletions
@@ -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<FileRepository>()
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 }
}
@@ -1,146 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.List;
public abstract class OldBaseModule implements Comparable<OldBaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1;
protected OldBaseModule() {
mId = mName = mVersion = mAuthor = mDescription = "";
}
protected OldBaseModule(Cursor c) {
mId = nonNull(c.getString(c.getColumnIndex("id")));
mName = nonNull(c.getString(c.getColumnIndex("name")));
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
}
protected OldBaseModule(Parcel p) {
mId = p.readString();
mName = p.readString();
mVersion = p.readString();
mAuthor = p.readString();
mDescription = p.readString();
mVersionCode = p.readInt();
}
@Override
public int compareTo(@NonNull OldBaseModule module) {
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mId);
dest.writeString(mName);
dest.writeString(mVersion);
dest.writeString(mAuthor);
dest.writeString(mDescription);
dest.writeInt(mVersionCode);
}
private String nonNull(String s) {
return s == null ? "" : s;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("id", mId);
values.put("name", mName);
values.put("version", mVersion);
values.put("versionCode", mVersionCode);
values.put("author", mAuthor);
values.put("description", mDescription);
return values;
}
protected void parseProps(List<String> props) {
parseProps(props.toArray(new String[0]));
}
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2)
continue;
String key = prop[0].trim();
String value = prop[1].trim();
if (key.isEmpty() || key.charAt(0) == '#')
continue;
switch (key) {
case "id":
mId = value;
break;
case "name":
mName = value;
break;
case "version":
mVersion = value;
break;
case "versionCode":
mVersionCode = Integer.parseInt(value);
break;
case "author":
mAuthor = value;
break;
case "description":
mDescription = value;
break;
default:
break;
}
}
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
}
@@ -1,110 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.XStringKt;
import java.text.DateFormat;
import java.util.Date;
public class Repo extends OldBaseModule {
private Date mLastUpdate;
public Repo(String id) {
setId(id);
}
public Repo(Cursor c) {
super(c);
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
}
public Repo(Parcel p) {
super(p);
mLastUpdate = new Date(p.readLong());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override
public Repo createFromParcel(Parcel source) {
return new Repo(source);
}
@Override
public Repo[] newArray(int size) {
return new Repo[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(mLastUpdate.getTime());
}
public void update() throws IllegalRepoException {
String[] props = Utils.INSTANCE.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
}
if (getVersionCode() < 0) {
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
}
}
public void update(Date lastUpdate) throws IllegalRepoException {
mLastUpdate = lastUpdate;
update();
}
@Override
public ContentValues getContentValues() {
ContentValues values = super.getContentValues();
values.put("last_update", mLastUpdate.getTime());
return values;
}
public String getZipUrl() {
return String.format(Const.Url.ZIP_URL, getId());
}
public String getPropUrl() {
return getFileUrl("module.prop");
}
public String getDetailUrl() {
return getFileUrl("README.md");
}
public String getFileUrl(String file) {
return String.format(Const.Url.FILE_URL, getId(), file);
}
public String getLastUpdateString() {
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
}
public Date getLastUpdate() {
return mLastUpdate;
}
public String getDownloadFilename() {
return XStringKt.legalFilename(getName() + "-" + getVersion() + ".zip");
}
public static class IllegalRepoException extends Exception {
IllegalRepoException(String message) {
super(message);
}
}
}
@@ -5,7 +5,7 @@ import android.os.Parcelable
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.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
@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.model.entity.module
abstract class BaseModule : Comparable<BaseModule> {
abstract var id: String
protected set
abstract var name: String
protected set
abstract var author: String
protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
}
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
}
@@ -1,50 +1,10 @@
package com.topjohnwu.magisk.model.entity
package com.topjohnwu.magisk.model.entity.module
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Const
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
abstract class BaseModule : Comparable<BaseModule> {
abstract var id: String
protected set
abstract var name: String
protected set
abstract var author: String
protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
}
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
}
class Module(path: String) : BaseModule() {
override var id: String = ""
override var name: String = ""
@@ -0,0 +1,68 @@
package com.topjohnwu.magisk.model.entity.module
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.legalFilename
import kotlinx.android.parcel.Parcelize
import java.text.DateFormat
import java.util.*
@Entity(tableName = "repos")
@Parcelize
data class Repo(
@PrimaryKey override var id: String,
override var name: String,
override var author: String,
override var version: String,
override var versionCode: Int,
override var description: String,
var last_update: Long
) : BaseModule(), Parcelable {
private val stringRepo: StringRepository get() = get()
val lastUpdate get() = Date(last_update)
val lastUpdateString: String get() =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(lastUpdate)
val downloadFilename: String get() = "$name-$version.zip".legalFilename()
val readme get() = stringRepo.getReadme(this)
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
constructor(id: String) : this(id, "", "", "", -1, "", 0)
@Throws(IllegalRepoException::class)
fun update() {
val props = runCatching {
stringRepo.getMetadata(this).blockingGet()
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
}.getOrElse {
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
}
props.runCatching {
parseProps(this)
}.onFailure {
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
}
if (versionCode < 0) {
throw IllegalRepoException("Repo [$id] does not contain versionCode")
}
}
@Throws(IllegalRepoException::class)
fun update(lastUpdate: Date) {
last_update = lastUpdate.time
update()
}
class IllegalRepoException(message: String) : Exception(message)
}
@@ -6,8 +6,8 @@ import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.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<RepoRvItem>() {
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
}
@@ -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