From e17e839f2ddb97b59c6c00609b7bce35efb369a4 Mon Sep 17 00:00:00 2001 From: nift4 Date: Tue, 3 Jan 2023 18:01:11 +0100 Subject: [PATCH] improve zip handler --- .../mmm/utils/BudgetProgressDialog.java | 49 +++++ .../java/com/fox2code/mmm/utils/Files.java | 55 +++++ .../com/fox2code/mmm/utils/PropUtils.java | 10 +- .../com/fox2code/mmm/utils/ZipFileOpener.java | 207 ++++++++++++------ app/src/main/res/values/strings.xml | 9 +- 5 files changed, 260 insertions(+), 70 deletions(-) create mode 100644 app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.java diff --git a/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.java b/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.java new file mode 100644 index 0000000..95c5d05 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.java @@ -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)); + } +} diff --git a/app/src/main/java/com/fox2code/mmm/utils/Files.java b/app/src/main/java/com/fox2code/mmm/utils/Files.java index 53f25e1..e1696ab 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Files.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Files.java @@ -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); diff --git a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java index 568f76f..927a677 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -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); diff --git a/app/src/main/java/com/fox2code/mmm/utils/ZipFileOpener.java b/app/src/main/java/com/fox2code/mmm/utils/ZipFileOpener.java index 30e697b..9e74b53 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/ZipFileOpener.java +++ b/app/src/main/java/com/fox2code/mmm/utils/ZipFileOpener.java @@ -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(); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f17f72..ce85db8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,5 +269,12 @@ Your webview is outdated! Please update it.Don\'t see your language?Help us by translating it! Tap here to find out more. Commit %1$s @ %2$s No file was provided when trying to open zip. - Could not load the zip fileDeveloped in part by AndroidacyHuge shoutout to Androidacy for their integration and contributions to the app.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. + Could not load the zip file + Could not load the module properties + Security warning + Do you really want to install the module \"%s\" from the ZIP file \"%s\"? Installing untrusted modules can lead to security risks. + Developed in part by Androidacy + Huge shoutout to Androidacy for their integration and contributions to the app. + 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. + Inspecting moduleā€¦