improve zip handler

pull/273/head
nift4 1 year ago
parent dddbf6d1c7
commit e17e839f2d

@ -0,0 +1,49 @@
package com.fox2code.mmm.utils;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.LinearLayoutCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
// ProgressDialog is deprecated because it's an bad UX pattern, but sometimes we have no other choice...
public class BudgetProgressDialog {
public static AlertDialog build(Context context, String title, String message) {
Resources r = context.getResources();
int padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, r.getDisplayMetrics()));
LinearLayoutCompat v = new LinearLayoutCompat(context);
v.setOrientation(LinearLayoutCompat.HORIZONTAL);
ProgressBar pb = new ProgressBar(context);
v.addView(pb, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
TextView t = new TextView(context);
t.setGravity(Gravity.CENTER);
v.addView(t, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 4));
v.setPadding(padding, padding, padding, padding);
t.setText(message);
return new MaterialAlertDialogBuilder(context)
.setTitle(title)
.setView(v)
.setCancelable(false)
.create();
}
public static AlertDialog build(Context context, int title, String message) {
return build(context, context.getString(title), message);
}
public static AlertDialog build(Context context, String title, int message) {
return build(context, title, context.getString(message));
}
public static AlertDialog build(Context context, int title, int message) {
return build(context, title, context.getString(message));
}
}

@ -1,8 +1,14 @@
package com.fox2code.mmm.utils;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -24,6 +30,55 @@ import java.util.zip.ZipOutputStream;
public class Files {
private static final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
// stolen from https://stackoverflow.com/a/25005243
public static @NonNull String getFileName(Context context, Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index != -1) {
result = cursor.getString(index);
}
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
// based on https://stackoverflow.com/a/63018108
public static @Nullable Long getFileSize(Context context, Uri uri) {
Long result = null;
try {
String scheme = uri.getScheme();
if (scheme.equals("content")) {
Cursor returnCursor = context.getContentResolver().
query(uri, null, null, null, null);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
long size = returnCursor.getLong(sizeIndex);
returnCursor.close();
result = size;
}
if (scheme.equals("file")) {
result = new File(uri.getPath()).length();
}
} catch (Exception e) {
Log.e("Files", Log.getStackTraceString(e));
return result;
}
return result;
}
public static void write(File file, byte[] bytes) throws IOException {
try (OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(bytes);

@ -328,7 +328,7 @@ public class PropUtils {
}
}
public static String readModuleId(InputStream inputStream) {
public static String readModulePropSimple(InputStream inputStream, String what) {
if (inputStream == null) return null;
String moduleId = null;
try (BufferedReader bufferedReader = new BufferedReader(
@ -337,8 +337,8 @@ public class PropUtils {
while ((line = bufferedReader.readLine()) != null) {
while (line.startsWith("\u0000"))
line = line.substring(1);
if (line.startsWith("id=")) {
moduleId = line.substring(3).trim();
if (line.startsWith(what + "=")) {
moduleId = line.substring(what.length() + 1).trim();
}
}
} catch (IOException e) {
@ -347,6 +347,10 @@ public class PropUtils {
return moduleId;
}
public static String readModuleId(InputStream inputStream) {
return readModulePropSimple(inputStream, "id");
}
public static void applyFallbacks(ModuleInfo moduleInfo) {
if (moduleInfo.support == null || moduleInfo.support.isEmpty()) {
moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id);

@ -1,101 +1,176 @@
package com.fox2code.mmm.utils;
import static androidx.fragment.app.FragmentManager.TAG;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.R;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ZipFileOpener extends FoxActivity {
AlertDialog loading = null;
// Adds us as a handler for zip files, so we can pass them to the installer
// We should have a content uri provided to us.
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
Log.d("ZipFileOpener", "onCreate: " + getIntent());
}
File zipFile;
Uri uri = getIntent().getData();
if (uri == null) {
Log.e("ZipFileOpener", "onCreate: No data provided");
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finish();
return;
}
// Try to copy the file to our cache
try {
zipFile = File.createTempFile("module", ".zip", getCacheDir());
try (InputStream inputStream = getContentResolver().openInputStream(uri); FileOutputStream outputStream = new FileOutputStream(zipFile)) {
if (inputStream == null) {
Log.e(TAG, "onCreate: Failed to open input stream");
loading = BudgetProgressDialog.build(this, R.string.loading, R.string.zip_unpacking);
new Thread(() -> {
if (BuildConfig.DEBUG) {
Log.d("ZipFileOpener", "onCreate: " + getIntent());
}
File zipFile;
Uri uri = getIntent().getData();
if (uri == null) {
Log.e("ZipFileOpener", "onCreate: No data provided");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
// Try to copy the file to our cache
try {
// check if its a file over 10MB
Long fileSize = Files.getFileSize(this, uri);
if (fileSize == null) fileSize = 0L;
if (1000L * 1000 * 10 < fileSize) {
runOnUiThread(() -> loading.show());
}
zipFile = File.createTempFile("module", ".zip", getCacheDir());
try (InputStream inputStream = getContentResolver().openInputStream(uri); FileOutputStream outputStream = new FileOutputStream(zipFile)) {
if (inputStream == null) {
Log.e("ZipFileOpener", "onCreate: Failed to open input stream");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
}
} catch (
Exception e) {
Log.e("ZipFileOpener", "onCreate: Failed to copy zip file", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
// Ensure zip is not empty
if (zipFile.length() == 0) {
Log.e("ZipFileOpener", "onCreate: Zip file is empty");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
} else {
if (BuildConfig.DEBUG) {
Log.d("ZipFileOpener", "onCreate: Zip file is " + zipFile.length() + " bytes");
}
}
ZipEntry entry;
ZipFile zip = null;
// Unpack the zip to validate it's a valid magisk module
// It needs to have, at the bare minimum, a module.prop file. Everything else is technically optional.
// First, check if it's a zip file
try {
zip = new ZipFile(zipFile);
if ((entry = zip.getEntry("module.prop")) == null) {
Log.e("ZipFileOpener", "onCreate: Zip file is not a valid magisk module");
runOnUiThread(() -> {
Toast.makeText(this, R.string.invalid_format, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
} catch (
Exception e) {
Log.e("ZipFileOpener", "onCreate: Failed to open zip file", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
if (zip != null) {
try {
zip.close();
} catch (IOException exception) {
Log.e("ZipFileOpener", Log.getStackTraceString(exception));
}
}
return;
}
} catch (
Exception e) {
Log.e(TAG, "onCreate: Failed to copy zip file", e);
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
return;
}
// Ensure zip is not empty
if (zipFile.length() == 0) {
Log.e(TAG, "onCreate: Zip file is empty");
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
return;
} else {
if (BuildConfig.DEBUG) {
Log.d("ZipFileOpener", "onCreate: Zip file is " + zipFile.length() + " bytes");
Log.d("ZipFileOpener", "onCreate: Zip file is valid");
}
}
// Unpack the zip to validate it's a valid magisk module
// It needs to have, at the bare minimum, a module.prop file. Everything else is technically optional.
// First, check if it's a zip file
try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(zipFile)) {
if (zip.getEntry("module.prop") == null) {
Log.e(TAG, "onCreate: Zip file is not a valid magisk module");
Toast.makeText(this, R.string.invalid_format, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
String moduleInfo;
try {
moduleInfo = PropUtils.readModulePropSimple(zip.getInputStream(entry), "name");
if (moduleInfo == null) {
throw new NullPointerException("moduleInfo is null, check earlier logs for root cause");
}
} catch (
Exception e) {
Log.e("ZipFileOpener", "onCreate: Failed to load module id", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_prop_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
try {
zip.close();
} catch (IOException exception) {
Log.e("ZipFileOpener", Log.getStackTraceString(exception));
}
return;
}
} catch (
IOException e) {
Log.e(TAG, "onCreate: Failed to open zip file", e);
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
return;
}
if (BuildConfig.DEBUG) {
Log.d("ZipFileOpener", "onCreate: Zip file is valid");
}
// Pass the file to the installer
FoxActivity compatActivity = FoxActivity.getFoxActivity(this);
IntentHelper.openInstaller(compatActivity, zipFile.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
try {
zip.close();
} catch (IOException exception) {
Log.e("ZipFileOpener", Log.getStackTraceString(exception));
}
runOnUiThread(() -> {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.zip_security_warning)
.setMessage(getString(R.string.zip_intent_module_install, moduleInfo, Files.getFileName(this, uri)))
.setCancelable(false)
.setNegativeButton(R.string.no, (d, i) -> {
d.dismiss();
finishAndRemoveTask();
})
.setPositiveButton(R.string.yes, (d, i) -> {
d.dismiss();
// Pass the file to the installer
FoxActivity compatActivity = FoxActivity.getFoxActivity(this);
IntentHelper.openInstaller(compatActivity, zipFile.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
finish();
})
.show();
loading.dismiss();
});
}).start();
}
}

@ -269,5 +269,12 @@
<string name="androidacy_webview_update_required">Your webview is outdated! Please update it.</string><string name="language_cta">Don\'t see your language?</string><string name="language_cta_desc">Help us by translating it! Tap here to find out more.</string>
<string name="source_code_summary"><b>Commit</b> %1$s @ %2$s</string>
<string name="no_file_provided">No file was provided when trying to open zip.</string>
<string name="zip_load_failed">Could not load the zip file</string><string name="androidacy_thanks">Developed in part by Androidacy</string><string name="androidacy_thanks_desc">Huge shoutout to Androidacy for their integration and contributions to the app.</string><string name="contributors">And of course, thanks to all of our contributors, whether it\'s translations, code, or just being fun to hang out with! We love you all.</string>
<string name="zip_load_failed">Could not load the zip file</string>
<string name="zip_prop_load_failed">Could not load the module properties</string>
<string name="zip_security_warning">Security warning</string>
<string name="zip_intent_module_install">Do you really want to install the module \"%s\" from the ZIP file \"%s\"? Installing untrusted modules can lead to security risks.</string>
<string name="androidacy_thanks">Developed in part by Androidacy</string>
<string name="androidacy_thanks_desc">Huge shoutout to Androidacy for their integration and contributions to the app.</string>
<string name="contributors">And of course, thanks to all of our contributors, whether it\'s translations, code, or just being fun to hang out with! We love you all.</string>
<string name="zip_unpacking">Inspecting module…</string>
</resources>

Loading…
Cancel
Save