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 android:name="a.j" />
<service
android:name="a.k"
<service android:name="a.j"
android:exported="false" />
<!-- Hardcode GMS version -->

View File

@ -1,7 +1,7 @@
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 */
}

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
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
@ -17,8 +16,7 @@ object ClassMap {
FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadModuleService::class.java to a.j::class.java,
DownloadService::class.java to a.k::class.java,
DownloadService::class.java to a.j::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 com.skoumal.teanity.rxbus.RxBus
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
@ -17,6 +15,4 @@ val applicationModule = module {
factory(Protected) { get<App>().protectedContext }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
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.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.writeToCachedFile
import com.topjohnwu.magisk.utils.writeTo
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import okhttp3.ResponseBody
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import timber.log.Timber
import java.io.File
import java.io.InputStream
abstract class RemoteFileService : NotificationService() {
@ -73,19 +74,14 @@ abstract class RemoteFileService : NotificationService() {
}
private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
.map { it.toFile(subject.hashCode(), subject.fileName) }
.map { it.toStream(subject.hashCode()) }
.map {
when (subject) {
is Module -> {
update(subject.hashCode()) {
it.setContentText(getString(R.string.download_module))
.setProgress(0, 0, true)
}
get<ModuleTransformer> { parametersOf(subject) }
.inject(it, startInternal(Installer).blockingGet())
cachedFile(subject.fileName).apply {
when (subject) {
is Module -> it.toModule(this,
repo.downloadFile(Const.Url.MODULE_INSTALLER).blockingGet().byteStream())
else -> it.writeTo(this)
}
else -> it
}
}
@ -95,11 +91,11 @@ abstract class RemoteFileService : NotificationService() {
.firstOrNull { it == name }
?.let { File(this, it) }
private fun ResponseBody.toFile(id: Int, name: String): File {
private fun ResponseBody.toStream(id: Int): InputStream {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f
return writeToCachedFile(this@RemoteFileService, name) {
return ProgInputStream(byteStream()) {
val progress = it / 1_000_000f
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.MagiskFragment
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.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
@ -73,7 +73,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().copyTo(EXT_FILE) }
.map { it.byteStream().writeTo(EXT_FILE) }
.subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) {

View File

@ -95,10 +95,12 @@ fun File.provide(context: Context = get()): Uri {
}
fun File.mv(destination: File) {
inputStream().copyTo(destination)
inputStream().writeTo(destination)
deleteRecursively()
}
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 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
@ -18,12 +16,10 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
}
}
fun Uri.copyTo(file: File) = toFile().copyTo(file)
fun InputStream.copyTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
fun Uri.writeTo(file: File) = toFile().copyTo(file)
fun File.tarInputStream() = TarInputStream(inputStream())
fun File.tarOutputStream() = TarOutputStream(this)
fun InputStream.writeTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,

View File

@ -1,8 +1,10 @@
package com.topjohnwu.magisk.utils
import android.content.Context
import com.topjohnwu.superuser.internal.UiThreadHandler
import okhttp3.ResponseBody
import java.io.File
import java.io.FilterInputStream
import java.io.InputStream
import java.io.OutputStream
@ -32,8 +34,6 @@ inline fun InputStream.writeTo(output: OutputStream, progress: (Long) -> Unit =
fun ResponseBody.writeToString() = string()
fun Context.cachedFile(name: String) = File(cacheDir, name)
inline fun InputStream.copyToWithProgress(
out: OutputStream,
progressEmitter: (Long) -> Unit,
@ -49,4 +49,32 @@ inline fun InputStream.copyToWithProgress(
progressEmitter(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
}
}