diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d638d96..13b3113 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,9 @@ # Contributor Covenant Code of Conduct +Note: The code of conduct are the rules that applies to everyone interacting +with the repo, regardless of acceptance, moderators of the repo are not forced +to apply it strictly, so be nice! + ## Our Pledge We as members, contributors, and leaders pledge to make participation in our @@ -24,6 +28,8 @@ community include: and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community +* Respecting mistakes as something happening to everyone that is useful as + learning material Examples of unacceptable behavior include: @@ -31,8 +37,8 @@ Examples of unacceptable behavior include: advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission +* Publishing others' private information, such as a physical or email address, + with or without their explicit permission unless widely publicly available * Other conduct which could reasonably be considered inappropriate in a professional setting @@ -73,16 +79,16 @@ the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. +harmful or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. +behavior was inappropriate. ### 2. Warning **Community Impact**: A violation through a single incident or series -of actions. +of actions, or a behaviour that induce hatred in the community. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with diff --git a/app/build.gradle b/app/build.gradle index 6b3bf86..3a3abb3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.fox2code.mmm" minSdk 21 targetSdk 32 - versionCode 39 - versionName "0.4.4" + versionCode 40 + versionName "0.4.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/fox2code/mmm/NotificationType.java b/app/src/main/java/com/fox2code/mmm/NotificationType.java index 3dd913d..a92a784 100644 --- a/app/src/main/java/com/fox2code/mmm/NotificationType.java +++ b/app/src/main/java/com/fox2code/mmm/NotificationType.java @@ -83,14 +83,16 @@ public enum NotificationType implements NotificationTypeCst { try { boolean needPatch; try (ZipFile zipFile = new ZipFile(d)) { - needPatch = zipFile.getEntry("module.prop") == null; + needPatch = zipFile.getEntry("module.prop") == null && + zipFile.getEntry("anykernel.sh") == null; } if (needPatch) { Files.patchModuleSimple(Files.read(d), new FileOutputStream(d)); } try (ZipFile zipFile = new ZipFile(d)) { - needPatch = zipFile.getEntry("module.prop") == null; + needPatch = zipFile.getEntry("module.prop") == null && + zipFile.getEntry("anykernel.sh") == null; } if (needPatch) { if (d.exists() && !d.delete()) diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index 16ee97c..aa85393 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -37,11 +37,14 @@ import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import com.topjohnwu.superuser.io.SuFile; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.PrintStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; @@ -123,127 +126,106 @@ public class InstallerActivity extends CompatActivity { this.getWindow().setFlags( // Note: Doesn't require WAKELOCK permission WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - if (urlMode) { - this.progressIndicator.setVisibility(View.VISIBLE); - this.installerTerminal.addLine("- Downloading " + name); - new Thread(() -> { - File moduleCache = this.toDelete = - new File(this.moduleCache, "module.zip"); - if (moduleCache.exists() && !moduleCache.delete() && - !new SuFile(moduleCache.getAbsolutePath()).delete()) - Log.e(TAG, "Failed to delete module cache"); - String errMessage = "Failed to download module zip"; - try { - Log.i(TAG, "Downloading: " + target); - byte[] rawModule = Http.doHttpGet(target, (progress, max, done) -> { - if (max <= 0 && this.progressIndicator.isIndeterminate()) - return; - this.runOnUiThread(() -> { - this.progressIndicator.setIndeterminate(false); - this.progressIndicator.setMax(max); - this.progressIndicator.setProgressCompat(progress, true); - }); + this.progressIndicator.setVisibility(View.VISIBLE); + if (urlMode) this.installerTerminal.addLine("- Downloading " + name); + new Thread(() -> { + File moduleCache = this.toDelete = urlMode ? + new File(this.moduleCache, "module.zip") : new File(target); + if (urlMode && moduleCache.exists() && !moduleCache.delete() && + !new SuFile(moduleCache.getAbsolutePath()).delete()) + Log.e(TAG, "Failed to delete module cache"); + String errMessage = "Failed to download module zip"; + try { + Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target); + byte[] rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> { + if (max <= 0 && this.progressIndicator.isIndeterminate()) + return; + this.runOnUiThread(() -> { + this.progressIndicator.setIndeterminate(false); + this.progressIndicator.setMax(max); + this.progressIndicator.setProgressCompat(progress, true); }); + }) : Files.readSU(moduleCache); + this.runOnUiThread(() -> { + this.progressIndicator.setVisibility(View.GONE); + this.progressIndicator.setIndeterminate(true); + }); + if (this.canceled) return; + if (checksum != null && !checksum.isEmpty()) { + Log.d(TAG, "Checking for checksum: " + checksum); this.runOnUiThread(() -> { - this.progressIndicator.setVisibility(View.GONE); - this.progressIndicator.setIndeterminate(true); + this.installerTerminal.addLine("- Checking file integrity"); }); - if (this.canceled) return; - if (checksum != null && !checksum.isEmpty()) { - Log.d(TAG, "Checking for checksum: " + checksum); - this.runOnUiThread(() -> { - this.installerTerminal.addLine("- Checking file integrity"); - }); - if (!Hashes.checkSumMatch(rawModule, checksum)) { - this.setInstallStateFinished(false, - "! File integrity check failed", ""); - return; - } + if (!Hashes.checkSumMatch(rawModule, checksum)) { + this.setInstallStateFinished(false, + "! File integrity check failed", ""); + return; } - if (this.canceled) return; - Files.fixJavaZipHax(rawModule); - boolean noPatch = false; - boolean isModule = false; - boolean isAnyKernel = false; - errMessage = "File is not a valid zip file"; - try (ZipInputStream zipInputStream = new ZipInputStream( - new ByteArrayInputStream(rawModule))) { - ZipEntry zipEntry; - while ((zipEntry = zipInputStream.getNextEntry()) != null) { - String entryName = zipEntry.getName(); - if (entryName.equals("anykernel.sh")) { - isAnyKernel = true; - break; - } else if (entryName.equals("module.prop")) { - noPatch = true; - isModule = true; - break; - } else if (entryName.endsWith("/module.prop")) { - isModule = true; - } + } + if (this.canceled) return; + Files.fixJavaZipHax(rawModule); + boolean noPatch = false; + boolean isModule = false; + boolean isAnyKernel = false; + errMessage = "File is not a valid zip file"; + try (ZipInputStream zipInputStream = new ZipInputStream( + new ByteArrayInputStream(rawModule))) { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + String entryName = zipEntry.getName(); + if (entryName.equals("anykernel.sh")) { + noPatch = true; + isAnyKernel = true; + break; + } else if (entryName.equals("module.prop")) { + noPatch = true; + isModule = true; + break; + } else if (entryName.endsWith("/anykernel.sh")) { + isAnyKernel = true; + } else if (entryName.endsWith("/module.prop")) { + isModule = true; } } - if (!isModule) { - this.setInstallStateFinished(false, isAnyKernel ? - "! AnyKernel modules can only be installed on recovery" : - "! File is not a valid magisk module", ""); - return; - } - if (noPatch) { + } + if (!isModule && !isAnyKernel) { + this.setInstallStateFinished(false, + "! File is not a valid magisk module", ""); + return; + } + if (noPatch) { + if (urlMode) { errMessage = "Failed to save module zip"; try (OutputStream outputStream = new FileOutputStream(moduleCache)) { outputStream.write(rawModule); outputStream.flush(); } - } else { - errMessage = "Failed to patch module zip"; - this.runOnUiThread(() -> { - this.installerTerminal.addLine("- Patching " + name); - }); - Log.i(TAG, "Patching: " + moduleCache.getName()); - try (OutputStream outputStream = new FileOutputStream(moduleCache)) { - Files.patchModuleSimple(rawModule, outputStream); - outputStream.flush(); - } } - if (this.canceled) return; - //noinspection UnusedAssignment (Important to avoid OutOfMemoryError) - rawModule = null; // Because reference is kept when calling doInstall + } else { + errMessage = "Failed to patch module zip"; this.runOnUiThread(() -> { - this.installerTerminal.addLine("- Installing " + name); + this.installerTerminal.addLine("- Patching " + name); }); - errMessage = "Failed to install module zip"; - this.doInstall(moduleCache, noExtensions, rootless); - } catch (IOException e) { - Log.e(TAG, errMessage, e); - this.setInstallStateFinished(false, - "! " + errMessage, ""); - } - }, "Module download Thread").start(); - } else { - final File moduleFile = new File(target); - if (checksum != null && !checksum.isEmpty()) { - Log.d(TAG, "Checking for checksum: " + checksum); - this.installerTerminal.addLine("- Checking file integrity"); - try { - if (!Hashes.checkSumMatch(Files.readSU(moduleFile), checksum)) { - this.setInstallStateFinished(false, - "! File integrity check failed", ""); - return; + Log.i(TAG, "Patching: " + moduleCache.getName()); + try (OutputStream outputStream = new FileOutputStream(moduleCache)) { + Files.patchModuleSimple(rawModule, outputStream); + outputStream.flush(); } - } catch (IOException e) { - Log.e(TAG, "Failed to read file for checksum check", e); - this.setInstallStateFinished(false, - "! File integrity check failed", ""); - return; } + //noinspection UnusedAssignment (Important to avoid OutOfMemoryError) + rawModule = null; // Because reference is kept when calling doInstall if (this.canceled) return; + this.runOnUiThread(() -> { + this.installerTerminal.addLine("- Installing " + name); + }); + errMessage = "Failed to install module zip"; + this.doInstall(moduleCache, noExtensions, rootless); + } catch (IOException e) { + Log.e(TAG, errMessage, e); + this.setInstallStateFinished(false, + "! " + errMessage, ""); } - this.installerTerminal.addLine("- Installing " + name); - new Thread(() -> this.doInstall( - this.toDelete = moduleFile, noExtensions, rootless), - "Install Thread").start(); - } + }, "Module install Thread").start(); } @@ -276,17 +258,67 @@ public class InstallerActivity extends CompatActivity { String arch32 = "true"; // Do nothing by default boolean needs32bit = false; String moduleId = null; + boolean anyKernel = false; + boolean magiskModule = false; + boolean anyKernelSystemLess = false; + File anyKernelInstallScript = new File(this.moduleCache, "update-binary"); try (ZipFile zipFile = new ZipFile(file)) { + ZipEntry anyKernelSh = zipFile.getEntry("anykernel.sh"); + if (anyKernelSh != null) { // Check if module is AnyKernel module + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(zipFile.getInputStream(anyKernelSh))); + String line; + // Check if AnyKernel module support system-less + while ((line = bufferedReader.readLine()) != null) { + String trimmedLine = line.trim(); + if (trimmedLine.equals("do.modules=1")) + anyKernel = true; + if (trimmedLine.equals("do.systemless=1")) + anyKernelSystemLess = true; + } + bufferedReader.close(); + if (anyKernelSystemLess && anyKernel) { + anyKernelSystemLess = false; + ZipEntry updateBinary = zipFile.getEntry( + "META-INF/com/google/android/update-binary"); + if (updateBinary != null) { + bufferedReader = new BufferedReader( + new InputStreamReader(zipFile.getInputStream(updateBinary))); + PrintStream printStream = new PrintStream( + new FileOutputStream(anyKernelInstallScript)); + while ((line = bufferedReader.readLine()) != null) { + String trimmedLine = line.trim(); + if (trimmedLine.equals("mount_all;") || + trimmedLine.equals("umount_all;")) + continue; // Do not mount anything + line = line.replace("/sbin/sh", "/system/bin/sh"); + int prePatch = line.length(); + line = line.replace("/data/adb/modules/ak3-helper", + "/data/adb/modules-update/ak3-helper"); + if (prePatch != line.length()) anyKernelSystemLess = true; + printStream.println(line); + } + printStream.close(); + bufferedReader.close(); + if (!anyKernelSystemLess) anyKernelInstallScript.delete(); + } + } + anyKernel = true; + } if (zipFile.getEntry( // Check if module hard require 32bit support "common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null && zipFile.getEntry( "common/addon/Volume-Key-Selector/install.sh") != null) { needs32bit = true; } + ZipEntry moduleProp = zipFile.getEntry("module.prop"); + magiskModule = moduleProp != null; moduleId = PropUtils.readModuleId(zipFile .getInputStream(zipFile.getEntry("module.prop"))); } catch (IOException ignored) { } + .getInputStream(moduleProp)); + } catch (IOException ignored) {} int compatFlags = AppUpdateManager.getFlagsForModule(moduleId); if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NEED_32BIT) != 0) needs32bit = true; @@ -300,6 +332,12 @@ public class InstallerActivity extends CompatActivity { null); return; } + if (magiskModule && moduleId == null && !anyKernel) { + // Modules without module Ids are module installed by 3rd party software + this.setInstallStateFinished(false, + "! Magisk modules require a moduleId", null); + return; + } if (Build.SUPPORTED_32_BIT_ABIS.length == 0) { if (needs32bit) { this.setInstallStateFinished(false, @@ -316,14 +354,23 @@ public class InstallerActivity extends CompatActivity { } String installCommand; File installExecutable; - if (InstallerInitializer.peekMagiskVersion() >= + if (anyKernel && moduleId == null) { // AnyKernel modules don't have a moduleId + if (!anyKernelSystemLess) { + this.setInstallStateFinished(false, + "! This AnyKernel module only support recovery install", null); + return; + } + installExecutable = anyKernelInstallScript; + installCommand = "sh \"" + installExecutable.getAbsolutePath() + "\"" + + " /dev/null 0 \"" + file.getAbsolutePath() + "\""; + } else if (InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && ((compatFlags & AppUpdateManager.FLAG_COMPAT_MAGISK_CMD) != 0 || noExtensions || MainApplication.isUsingMagiskCommand())) { installCommand = "magisk --install-module \"" + file.getAbsolutePath() + "\""; installExecutable = new File(InstallerInitializer.peekMagiskPath() .equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk"); - } else { + } else if (moduleId != null) { installExecutable = this.extractInstallScript("module_installer_compat.sh"); if (installExecutable == null) { this.setInstallStateFinished(false, @@ -331,7 +378,11 @@ public class InstallerActivity extends CompatActivity { return; } installCommand = "sh \"" + installExecutable.getAbsolutePath() + "\"" + - " /dev/null 1 \"" + file.getAbsolutePath() + "\""; + " /dev/null 0 \"" + file.getAbsolutePath() + "\""; + } else { + this.setInstallStateFinished(false, + "! Zip file is not a valid Magisk or a AnyKernel module!", null); + return; } installerMonitor = new InstallerMonitor(installExecutable); if (moduleId != null) installerMonitor.setForCleanUp(moduleId); 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 edf6b83..d6d9ae1 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Files.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Files.java @@ -1,6 +1,7 @@ package com.fox2code.mmm.utils; import android.os.Build; +import android.util.Log; import androidx.annotation.NonNull; @@ -45,6 +46,11 @@ public class Files { } public static byte[] readSU(File file) throws IOException { + if (file.isFile() && file.canRead()) { + try { // Read as app if su not required + return read(file); + } catch (IOException ignored) {} + } try (InputStream inputStream = SuFileInputStream.open(file)) { return readAllBytes(inputStream); } diff --git a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java index 4d751e0..0026fd1 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -371,6 +371,7 @@ public class IntentHelper { } outputStream = new FileOutputStream(destination); Files.copy(inputStream, outputStream); + Log.d(TAG, "File saved at " + destination); success = true; } catch (Exception e) { Log.e(TAG, "failed copy of " + uri, e); 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 e680465..63639c2 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -303,6 +303,7 @@ public class PropUtils { } public static String readModuleId(InputStream inputStream) { + if (inputStream == null) return null; String moduleId = null; try (BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57b8f14..3c9a315 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,23 +1,23 @@ Fox\'s Magisk Module Manager Fox\'s Mmm - Failed to get access to Root or Magisk + Could not access either Root or Magisk Loading… - Updatable + Upgradable Installed Online Repo - The application is in lockdown mode - Failed to download file. + The app is in lockdown mode + Could not download the file Modules took too long to boot, consider disabling some modules - Failed to connect to the internet - Failed to get system WebView + Could not connect to the Internet + Could not open system WebView SettingsActivity - Application update available - Update + A new version of the app is available + Upgrade No description found. Download module Install module - Update module + Upgrade module Changelog Website Support @@ -29,7 +29,7 @@ No - Last update: + Last version: Repo: by Downloads: @@ -42,65 +42,68 @@ Lockdown mode prevent manager to do action on modules Prevent reboot Prevents unexpected reboots + Lockdown mode prevents the manager from carrying out actions on modules Settings Info Show licenses Licences Show incompatible modules - Show modules that are incompatible with your device based on their metadata - Magisk is outdated! + Show modules unlikely to work on your device based on their metadata + There is a new version of Magisk to install! Repos Security Appearance General The repository hosting Magisk Modules An alternative to Magisk-Modules-Repo with fewer restrictions. + The repository hosting Magisk modules + An alternative to the Magisk-Modules-Repo with fewer restrictions. Delete the module files? - Keep files - Delete files - Failed to delete the module files + Keep + Delete + Could not delete the module files Theme Theme mode - Module id: + Module ID: Install module from storage - The selected module is in an invalid format + The selected module has an invalid format Local install Source code - Magisk builtin module - Substratum builtin module - Force dark mode terminal - Your current file picker failed to give access to the file. + Magisk built-in module + Substratum built-in module + Dark terminal + Your current file picker could not access the file. Remote install - Your file picker returned a non standard response. - Use magisk module install command + Your file picker returned a non-standard response. + Use the \"magisk --install-module\" command - During test it caused problems to the module install error diagnosis tool, - so I hid this option behind developer mode, enable this at your own risk! + During testing it caused problems to the module install error diagnosis tool, + so this option behind is hidden behind developer mode.\nTurn this on at your own risk! - Developer mode enabled - Force English language - Disable low quality module filter + Developer mode on + English app language + Show low-quality modules - Some modules do not declare their metadata properly,causing visual glitches, - and/or indicating poor module quality, disable at your own risk! + Some modules do not declare their metadata properly, causing visual glitches, + and/or indicating poor module quality.\nTurn this off at your own risk! - Dns over https + DNS over HTTPS May fix connections issues in some cases. - (Does not apply to WebView) + (Does not apply to WebView.) - Disable extensions + No Mmm - Disable Fox\'s Mmm extensions, this prevent modules from using - terminal extensions, useful if a module misuse Fox\'s Mmm extensions. + Turn off Fox\'s Mmm extensions, preventing modules from using + terminal extensions.\nUseful if a module misuses Fox\'s Mmm extensions. - Wrap text + Text wrapping - Wrap text to a new line instead of putting - all text on the same line when installing a module + Show text on multiple lines instead of putting + all text on the same line when installing a module. - Enable blur - Repo enabled - Repo disabled - Androidacy repo uses ads and trackers. + Blur + Repo on + Repo off + The Androidacy repo has ads and trackers.