Stream and process module zips

This commit is contained in:
topjohnwu 2019-07-20 21:04:06 -07:00
parent 746a1d8d59
commit 8ca188f4d4
13 changed files with 99 additions and 270 deletions

View File

@ -65,9 +65,7 @@
<!-- Service --> <!-- Service -->
<service android:name="a.j" /> <service android:name="a.j"
<service
android:name="a.k"
android:exported="false" /> android:exported="false" />
<!-- Hardcode GMS version --> <!-- Hardcode GMS version -->

View File

@ -1,7 +1,7 @@
package a; package a;
import com.topjohnwu.magisk.model.download.DownloadModuleService; import com.topjohnwu.magisk.model.download.DownloadService;
public class j extends DownloadModuleService { public class j extends DownloadService {
/* stub */ /* stub */
} }

View File

@ -1,5 +0,0 @@
package a
import com.topjohnwu.magisk.model.download.DownloadService
class k : DownloadService()

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk package com.topjohnwu.magisk
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService import com.topjohnwu.magisk.model.update.UpdateCheckService
@ -17,8 +16,7 @@ object ClassMap {
FlashActivity::class.java to a.f::class.java, FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::class.java, UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java, GeneralReceiver::class.java to a.h::class.java,
DownloadModuleService::class.java to a.j::class.java, DownloadService::class.java to a.j::class.java,
DownloadService::class.java to a.k::class.java,
SuRequestActivity::class.java to a.m::class.java SuRequestActivity::class.java to a.m::class.java
) )

View File

@ -4,8 +4,6 @@ import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.skoumal.teanity.rxbus.RxBus import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.App import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.model.download.ModuleTransformer
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import org.koin.dsl.module import org.koin.dsl.module
@ -17,6 +15,4 @@ val applicationModule = module {
factory(Protected) { get<App>().protectedContext } factory(Protected) { get<App>().protectedContext }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) } single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) } single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
factory { (subject: DownloadSubject) -> ModuleTransformer(get(), subject) }
} }

View File

@ -1,156 +0,0 @@
package com.topjohnwu.magisk.model.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import androidx.annotation.Nullable;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.ProgressNotification;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
notifications = new ArrayList<>();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Shell.EXECUTOR.execute(() -> {
Repo repo = intent.getParcelableExtra("repo");
boolean install = intent.getBooleanExtra("install", false);
dlProcessInstall(repo, install);
});
return START_REDELIVER_INTENT;
}
@Override
public synchronized void onTaskRemoved(Intent rootIntent) {
for (ProgressNotification n : notifications) {
Notifications.mgr.cancel(n.hashCode());
}
notifications.clear();
}
private synchronized void addNotification(ProgressNotification n) {
if (notifications.isEmpty()) {
// Start foreground
startForeground(n.hashCode(), n.getNotification());
}
notifications.add(n);
}
private synchronized void removeNotification(ProgressNotification n) {
notifications.remove(n);
if (notifications.isEmpty()) {
// No more tasks, stop service
stopForeground(true);
stopSelf();
} else {
// Pick another notification as our foreground notification
n = notifications.get(0);
startForeground(n.hashCode(), n.getNotification());
}
}
private void dlProcessInstall(Repo repo, boolean install) {
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
ProgressNotification progress = new ProgressNotification(output.getName());
addNotification(progress);
try {
InputStream in = Networking.get(repo.getZipUrl())
.setDownloadProgressListener(progress)
.execForInputStream().getResult();
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
processZip(in, out);
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
intent.setData(Uri.fromFile(output))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
synchronized (getApplication()) {
if (install && App.foreground() != null &&
!(App.foreground() instanceof FlashActivity)) {
/* Only start flashing if there is a foreground activity and the
* user is not also flashing another module at the same time */
App.foreground().startActivity(intent);
} else {
/* Or else we preset a notification notifying that we are done */
PendingIntent pi = PendingIntent.getActivity(this, progress.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
progress.dlDone(pi);
}
}
} catch (Exception e) {
e.printStackTrace();
progress.dlFail();
}
removeNotification(progress);
}
private void processZip(InputStream in, OutputStream out)
throws IOException {
try (ZipInputStream zin = new ZipInputStream(in);
ZipOutputStream zout = new ZipOutputStream(out)) {
// Inject latest module-installer.sh as update-binary
zout.putNextEntry(new ZipEntry("META-INF/"));
zout.putNextEntry(new ZipEntry("META-INF/com/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
.execForInputStream().getResult()) {
ShellUtils.pump(update_bin, zout);
}
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
zout.write("#MAGISK\n".getBytes("UTF-8"));
int off = -1;
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
if (off < 0)
off = entry.getName().indexOf('/') + 1;
String path = entry.getName().substring(off);
if (path.isEmpty())
continue;
if (path.startsWith("META-INF"))
continue;
zout.putNextEntry(new ZipEntry(path));
if (!entry.isDirectory())
ShellUtils.pump(zin, zout);
}
}
}
}

View File

@ -0,0 +1,44 @@
package com.topjohnwu.magisk.model.download
import com.topjohnwu.magisk.utils.withStreams
import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
fun InputStream.toModule(file: File, installer: InputStream) {
val input = ZipInputStream(buffered())
val output = ZipOutputStream(file.outputStream().buffered())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
installer.copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
var entry: ZipEntry? = zin.nextEntry
while (entry != null) {
if (off < 0) {
off = entry.name.indexOf('/') + 1
}
val path = entry.name.substring(off)
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
entry = zin.nextEntry
}
}
}

View File

@ -1,68 +0,0 @@
package com.topjohnwu.magisk.model.download
import android.content.Context
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.cachedFile
import com.topjohnwu.magisk.utils.withStreams
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class ModuleTransformer(
private val context: Context,
subject: DownloadSubject
) {
private val destination = context.cachedFile(subject.fileName)
fun inject(file: File, installer: File): File {
return injectInternal(move(file), installer)
}
// ---
private fun injectInternal(file: File, installer: File): File {
val input = ZipInputStream(file.inputStream())
val output = ZipOutputStream(destination.outputStream())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
installer.inputStream().copyTo(zout).also { zout.flush() }
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
var entry: ZipEntry? = zin.nextEntry
while (entry != null) {
if (off < 0) {
off = entry.name.indexOf('/') + 1
}
val path = entry.name.substring(off)
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout).also { zout.flush() }
}
}
entry = zin.nextEntry
}
}
file.delete()
return destination
}
private fun move(file: File) = context.cachedFile("temp").apply {
file.renameTo(this)
}
}

View File

@ -9,18 +9,19 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.FileRepository import com.topjohnwu.magisk.data.repository.FileRepository
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.utils.ProgInputStream
import com.topjohnwu.magisk.utils.cachedFile
import com.topjohnwu.magisk.utils.firstMap import com.topjohnwu.magisk.utils.firstMap
import com.topjohnwu.magisk.utils.writeToCachedFile import com.topjohnwu.magisk.utils.writeTo
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.InputStream
abstract class RemoteFileService : NotificationService() { abstract class RemoteFileService : NotificationService() {
@ -73,19 +74,14 @@ abstract class RemoteFileService : NotificationService() {
} }
private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url) private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
.map { it.toFile(subject.hashCode(), subject.fileName) } .map { it.toStream(subject.hashCode()) }
.map { .map {
cachedFile(subject.fileName).apply {
when (subject) { when (subject) {
is Module -> { is Module -> it.toModule(this,
update(subject.hashCode()) { repo.downloadFile(Const.Url.MODULE_INSTALLER).blockingGet().byteStream())
it.setContentText(getString(R.string.download_module)) else -> it.writeTo(this)
.setProgress(0, 0, true)
} }
get<ModuleTransformer> { parametersOf(subject) }
.inject(it, startInternal(Installer).blockingGet())
}
else -> it
} }
} }
@ -95,11 +91,11 @@ abstract class RemoteFileService : NotificationService() {
.firstOrNull { it == name } .firstOrNull { it == name }
?.let { File(this, it) } ?.let { File(this, it) }
private fun ResponseBody.toFile(id: Int, name: String): File { private fun ResponseBody.toStream(id: Int): InputStream {
val maxRaw = contentLength() val maxRaw = contentLength()
val max = maxRaw / 1_000_000f val max = maxRaw / 1_000_000f
return writeToCachedFile(this@RemoteFileService, name) { return ProgInputStream(byteStream()) {
val progress = it / 1_000_000f val progress = it / 1_000_000f
update(id) { notification -> update(id) { notification ->

View File

@ -11,7 +11,7 @@ import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.ui.base.MagiskActivity import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.base.MagiskFragment import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.utils.ISafetyNetHelper import com.topjohnwu.magisk.utils.ISafetyNetHelper
import com.topjohnwu.magisk.utils.copyTo import com.topjohnwu.magisk.utils.writeTo
import com.topjohnwu.magisk.utils.inject import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.* import com.topjohnwu.magisk.view.dialogs.*
@ -73,7 +73,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private fun downloadSafetyNet(requiresUserInput: Boolean = true) { private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = magiskRepo.fetchSafetynet() fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().copyTo(EXT_FILE) } .map { it.byteStream().writeTo(EXT_FILE) }
.subscribeK { updateSafetyNet(true) } .subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) { if (!requiresUserInput) {

View File

@ -95,10 +95,12 @@ fun File.provide(context: Context = get()): Uri {
} }
fun File.mv(destination: File) { fun File.mv(destination: File) {
inputStream().copyTo(destination) inputStream().writeTo(destination)
deleteRecursively() deleteRecursively()
} }
fun String.toFile() = File(this) fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title) fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name)

View File

@ -2,8 +2,6 @@ package com.topjohnwu.magisk.utils
import android.net.Uri import android.net.Uri
import androidx.core.net.toFile import androidx.core.net.toFile
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -18,12 +16,10 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
} }
} }
fun Uri.copyTo(file: File) = toFile().copyTo(file) fun Uri.writeTo(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 InputStream.writeTo(file: File) =
fun File.tarOutputStream() = TarOutputStream(this) withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
inline fun <In : InputStream, Out : OutputStream> withStreams( inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In, inStream: In,

View File

@ -1,8 +1,10 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.utils
import android.content.Context import android.content.Context
import com.topjohnwu.superuser.internal.UiThreadHandler
import okhttp3.ResponseBody import okhttp3.ResponseBody
import java.io.File import java.io.File
import java.io.FilterInputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -32,8 +34,6 @@ inline fun InputStream.writeTo(output: OutputStream, progress: (Long) -> Unit =
fun ResponseBody.writeToString() = string() fun ResponseBody.writeToString() = string()
fun Context.cachedFile(name: String) = File(cacheDir, name)
inline fun InputStream.copyToWithProgress( inline fun InputStream.copyToWithProgress(
out: OutputStream, out: OutputStream,
progressEmitter: (Long) -> Unit, progressEmitter: (Long) -> Unit,
@ -50,3 +50,31 @@ inline fun InputStream.copyToWithProgress(
} }
return bytesCopied return bytesCopied
} }
class ProgInputStream(base: InputStream,
val progressEmitter: (Long) -> Unit = {}) : FilterInputStream(base) {
private var bytesRead : Long = 0
override fun read(): Int {
val b = read()
if (b >= 0) {
bytesRead++
UiThreadHandler.run { progressEmitter(bytesRead) }
}
return b
}
override fun read(b: ByteArray): Int {
return read(b, 0, b.size)
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
val sz = super.read(b, off, len)
if (sz > 0) {
bytesRead += sz
UiThreadHandler.run { progressEmitter(bytesRead) }
}
return sz
}
}