diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java index c081ddc4af7876c0edd9c2fcd4c0fbda6457c01d..0738d8866e9a0897d8e9ad186a9e0505577964a2 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java @@ -86,6 +86,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -119,6 +120,8 @@ import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; +import io.github.muntashirakon.AppManager.magisk.MagiskModule; +import io.github.muntashirakon.AppManager.magisk.MagiskModuleInfo; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.profiles.ProfileManager; import io.github.muntashirakon.AppManager.profiles.ProfileMetaManager; @@ -129,6 +132,7 @@ import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.scanner.ScannerActivity; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Ops; +import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.sharedpref.SharedPrefsActivity; import io.github.muntashirakon.AppManager.ssaid.ChangeSsaidDialog; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; @@ -648,8 +652,20 @@ public class AppInfoFragment extends Fragment implements SwipeRefreshLayout.OnRe }); } if (tagCloud.isSystemApp) { - if (tagCloud.isSystemlessPath) { - addChip(R.string.systemless_app); + if (tagCloud.systemlessPathInfo != null) { + addChip(R.string.systemless_app).setOnClickListener(v -> { + MagiskModuleInfo moduleInfo = tagCloud.systemlessPathInfo; + ScrollableDialogBuilder dialogBuilder = new ScrollableDialogBuilder(mActivity) + .setTitle(R.string.magisk_module_info_title) + .setMessage(moduleInfo.toLocalizedString(mActivity)) + .setNegativeButton(R.string.close, null); + if (Objects.equals(moduleInfo.id, MagiskModule.MODULE_NAME)) { + // This is App Manager module, offer to uninstall the app + dialogBuilder.setNeutralButton(R.string.uninstall, (dialog, which, isChecked) -> + doUninstallSystemlessAppWithAPrompt()); + } + dialogBuilder.show(); + }); } else addChip(R.string.system_app); if (tagCloud.isUpdatedSystemApp) { addChip(R.string.updated_app); @@ -835,6 +851,45 @@ public class AppInfoFragment extends Fragment implements SwipeRefreshLayout.OnRe } } + private void doUninstallSystemlessAppWithAPrompt() { + new ScrollableDialogBuilder(mActivity, R.string.uninstall_app_message) + .setTitle(mPackageLabel) + .setPositiveButton(R.string.uninstall, (dialog, which, keepData) -> + doUninstallSystemless(keepData)) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + private void doUninstallSystemless(boolean keepData) { + executor.submit(() -> { + boolean uninstalled = false; + String destPath = PackageUtils.getHiddenCodePathOrDefault(mApplicationInfo); + if (destPath != null) { + try { + MagiskModule magiskModule = MagiskModule.getInstance(); + magiskModule.uninstall(mPackageName, destPath); + PackageInstallerCompat installer = PackageInstallerCompat + .getNewInstance(); + installer.setAppLabel(mPackageLabel); + installer.uninstall(mPackageName, UserHandleHidden.USER_ALL, keepData); + uninstalled = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + boolean finalUninstalled = uninstalled; + runOnUiThread(() -> { + if (finalUninstalled) { + displayLongToast(R.string.uninstalled_successfully, mPackageLabel); + // TODO: 3/3/23 Ask for reboot + mActivity.finish(); + } else { + displayLongToast(R.string.failed_to_uninstall, mPackageLabel); + } + }); + }); + } + @UiThread private void displayMagiskHideDialog() { SearchableMultiChoiceDialogBuilder<MagiskProcess> builder; @@ -964,20 +1019,26 @@ public class AppInfoFragment extends Fragment implements SwipeRefreshLayout.OnRe ScrollableDialogBuilder builder = new ScrollableDialogBuilder(mActivity, isSystemApp ? R.string.uninstall_system_app_message : R.string.uninstall_app_message) .setTitle(mPackageLabel) - .setPositiveButton(R.string.uninstall, (dialog, which, keepData) -> executor.submit(() -> { - PackageInstallerCompat installer = PackageInstallerCompat - .getNewInstance(); - installer.setAppLabel(mPackageLabel); - boolean uninstalled = installer.uninstall(mPackageName, userId, keepData); - runOnUiThread(() -> { - if (uninstalled) { - displayLongToast(R.string.uninstalled_successfully, mPackageLabel); - mActivity.finish(); - } else { - displayLongToast(R.string.failed_to_uninstall, mPackageLabel); - } + .setPositiveButton(R.string.uninstall, (dialog, which, keepData) -> { + if (isSystemApp && Prefs.Installer.isSystemless()) { + doUninstallSystemless(keepData); + return; + } + executor.submit(() -> { + PackageInstallerCompat installer = PackageInstallerCompat + .getNewInstance(); + installer.setAppLabel(mPackageLabel); + boolean uninstalled = installer.uninstall(mPackageName, userId, keepData); + runOnUiThread(() -> { + if (uninstalled) { + displayLongToast(R.string.uninstalled_successfully, mPackageLabel); + mActivity.finish(); + } else { + displayLongToast(R.string.failed_to_uninstall, mPackageLabel); + } + }); }); - })) + }) .setNegativeButton(R.string.cancel, (dialog, which, keepData) -> { if (dialog != null) dialog.cancel(); }); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoViewModel.java index 6b119e9836a62df8dc7b411bb938c11d451e84f0..04a76c38b2041edd26a80625355fb2be863a8243 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoViewModel.java @@ -44,6 +44,7 @@ import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.details.AppDetailsViewModel; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; +import io.github.muntashirakon.AppManager.magisk.MagiskModuleInfo; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.magisk.MagiskUtils; import io.github.muntashirakon.AppManager.misc.OsEnvironment; @@ -138,9 +139,12 @@ public class AppInfoViewModel extends AndroidViewModel { tagCloud.areAllTrackersBlocked &= componentRule.isBlocked(); } tagCloud.isSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - tagCloud.isSystemlessPath = !mainModel.getIsExternalApk() && Ops.isRoot() - && MagiskUtils.isSystemlessPath(PackageUtils.getHiddenCodePathOrDefault(packageName, - applicationInfo.publicSourceDir)); + if (!mainModel.getIsExternalApk() && Ops.isRoot()) { + String codePath = PackageUtils.getHiddenCodePathOrDefault(applicationInfo); + if (codePath != null) { + tagCloud.systemlessPathInfo = MagiskUtils.getSystemlessPathInfo(codePath); + } + } tagCloud.isUpdatedSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; tagCloud.splitCount = mainModel.getSplitCount(); tagCloud.isDebuggable = (applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; @@ -374,7 +378,8 @@ public class AppInfoViewModel extends AndroidViewModel { public List<ComponentRule> trackerComponents; public boolean areAllTrackersBlocked = true; public boolean isSystemApp; - public boolean isSystemlessPath; + @Nullable + public MagiskModuleInfo systemlessPathInfo; public boolean isUpdatedSystemApp; public int splitCount; public boolean isDebuggable; diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModule.java b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModule.java new file mode 100644 index 0000000000000000000000000000000000000000..38b6519947881ad0b398af877efd0694c8f4bbd0 --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModule.java @@ -0,0 +1,267 @@ +package io.github.muntashirakon.AppManager.magisk; + +import android.system.ErrnoException; + +import androidx.annotation.NonNull; +import androidx.collection.ArraySet; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +import io.github.muntashirakon.AppManager.utils.ContextUtils; +import io.github.muntashirakon.AppManager.utils.ExUtils; +import io.github.muntashirakon.io.Path; +import io.github.muntashirakon.io.Paths; + +public class MagiskModule { + public static final String MODULE_NAME = "AppManager"; + public static final String MODULE_PROP = "module.prop"; + private static final String SCHEDULED_APK_FILE = "sched_apk.txt"; + + private static MagiskModule instance; + public static MagiskModule getInstance() throws IOException { + if (instance == null) { + try { + instance = new MagiskModule(); + } catch (ErrnoException e) { + ExUtils.rethrowAsIOException(e); + } + } + return instance; + } + + private static class PackageInfo { + public String packageName; + public String path; + public int type; + } + + @NonNull + private final Path modulePath; + + private MagiskModule() throws IOException, ErrnoException { + Path magiskModuleDir = MagiskUtils.getModDir(); + if (magiskModuleDir.hasFile(MODULE_NAME)) { + modulePath = magiskModuleDir.findFile(MODULE_NAME); + } else { + // Module does not yet exist + modulePath = magiskModuleDir.createNewDirectory(MODULE_NAME); + Paths.chmod(modulePath, 0755); + } + if (!modulePath.hasFile(MODULE_PROP)) { + Path moduleProp = modulePath.createNewFile(MODULE_PROP, null); + try (OutputStream os = moduleProp.openOutputStream()) { + generateModuleProp().store(os, null); + } + Paths.chmod(moduleProp, 0644); + } + } + + /** + * Install the APK file to the given destination. If the destination already exists, it attempts to replace it by + * calling {@link #replace(String, Path, String)}. This does not check whether the app is already installed. + * It is up to the caller to ensure that the app has already been installed. + * + * @param apkFile The APK file to put in the destination + * @param destination Destination with the APK name + */ + public void install(@NonNull String packageName, @NonNull Path apkFile, @NonNull String destination) + throws IOException { + destination = getSupportedPathOrFail(destination); + Path destPath = Paths.get(destination); + if (destPath.exists()) { + replace(packageName, apkFile, destination); + return; + } + // This works because it's a Linux FS + Path moduleDest = Objects.requireNonNull(Paths.build(modulePath, destination)); + if (moduleDest.exists()) { + throw new IOException(moduleDest + " exists"); + } + Path parent = moduleDest.getParentFile(); + if (parent == null) { + throw new FileNotFoundException("Parent not found"); + } + parent.mkdirs(); + Path newPath = apkFile.copyTo(parent); + if (newPath == null) { + throw new IOException("Could not copy " + apkFile + " into " + parent); + } + newPath.renameTo(moduleDest.getName()); + if (!Objects.equals(moduleDest, newPath)) { + newPath.delete(); + throw new IOException("Copy failed"); + } + // TODO: 3/3/23 Update module database to add this path + } + + /** + * Reinstall the APK file existed in the given destination. This only deletes the folder used as a placeholder in + * this module. It is up to the caller to reinstall the app using a given method. + * + * @param destination Destination with the APK name + */ + public void reinstall(@NonNull String packageName, @NonNull String destination) throws IOException { + destination = getSupportedPathOrFail(destination); + // This works because it's a Linux FS + Path moduleDest = Objects.requireNonNull(Paths.build(modulePath, destination)); + if (moduleDest.exists()) { + // The app is installed systemless-ly via App Manager, simply delete up to the last empty path + if (!deleteUntilNonEmpty(moduleDest)) { + throw new IOException("Could not delete " + moduleDest); + } + // TODO: 3/3/23 Update module database to remove this path + return; + } + throw new FileNotFoundException(destination + " not installed via App Manager"); + } + + /** + * Replace the old APK file with the given APK. If App Manager is already configured to use it, it simply replaces + * the APK with the new one. Otherwise, it overwrites the destination. A replaced APK is needed to be installed + * again after a reboot. + * + * @param apkFile The APK file to put in the destination + * @param destination Destination with the APK name + */ + public void replace(@NonNull String packageName, @NonNull Path apkFile, @NonNull String destination) throws IOException { + destination = getSupportedPathOrFail(destination); + Path realDest = Paths.get(destination); + if (!realDest.exists()) { + throw new FileNotFoundException(realDest + " must exist"); + } + // This works because it's a Linux FS + Path moduleDest = Objects.requireNonNull(Paths.build(modulePath, destination)); + Path parent = moduleDest.getParentFile(); + if (parent == null) { + throw new FileNotFoundException("Parent not found"); + } + if (!moduleDest.exists()) { + parent.mkdirs(); + } + Path newPath = apkFile.copyTo(parent); + if (newPath == null) { + throw new IOException("Could not copy " + apkFile + " into " + parent); + } + newPath.renameTo(moduleDest.getName()); + if (!Objects.equals(moduleDest, newPath)) { + newPath.delete(); + throw new IOException("Copy failed"); + } + // Schedule installation upon boot + Set<Path> paths = readScheduledApkFiles(); + paths.add(moduleDest); + writeScheduledApkFiles(paths); + // TODO: 3/3/23 Update module database to add this path + } + + /** + * If the app is installed systemless-ly via App Manager, the path is simply deleted. Otherwise, it overwrites + * the destination with an empty folder so that the APK appears to be unavailable. If the system app is updated, + * it might appear as a user app after a reboot. So, it is up to the caller to uninstall the app before calling + * this method. + * + * @param destination Destination without the APK name + */ + public void uninstall(@NonNull String packageName, @NonNull String destination) throws IOException { + destination = getSupportedPathOrFail(destination); + Path realDest = Paths.get(destination); + // This works because it's a Linux FS + Path moduleDest = Objects.requireNonNull(Paths.build(modulePath, destination)); + if (moduleDest.exists()) { + // The app is installed systemless-ly via App Manager, simply delete up to the last empty path + if (!deleteUntilNonEmpty(moduleDest)) { + throw new IOException("Could not delete " + moduleDest); + } + // TODO: 3/3/23 Update module database to remove this path + return; + } + // The app is not installed systemless-ly + if (realDest.exists()) { + // The destination exists, overwrite it in the module + if (!moduleDest.mkdirs()) { + throw new IOException("Could not create " + moduleDest); + } + moduleDest.createNewFile(".replace", null); + // TODO: 3/3/23 Update module database to add this path + return; + } + throw new FileNotFoundException(destination + " is already uninstalled"); + } + + @NonNull + private static String getSupportedPathOrFail(@NonNull String path) throws FileNotFoundException { + String newPath = MagiskUtils.getValidSystemLocation(path); + if (newPath == null) { + throw new FileNotFoundException("Invalid path " + path); + } + return newPath; + } + + private static boolean deleteUntilNonEmpty(@NonNull Path pathToDelete) { + if (!pathToDelete.exists()) { + return false; + } + Path parent = pathToDelete.getParentFile(); + // Delete the path first, regardless of whether it's a file or directory + boolean success = pathToDelete.delete(); + while (parent != null && parent.listFiles().length == 0) { + Path tmp = parent; + parent = parent.getParentFile(); + tmp.delete(); + } + return success; + } + + @NonNull + private static Properties generateModuleProp() { + Properties moduleProp = new Properties(); + moduleProp.put("id", MODULE_NAME); + moduleProp.put("name", "App Manager"); + moduleProp.put("version", "v1.0.0"); + moduleProp.put("versionCode", "1"); + moduleProp.put("author", "Muntashir Al-Islam"); + moduleProp.put("description", "Companion Magisk module for App Manager"); + return moduleProp; + } + + @NonNull + private static Set<Path> readScheduledApkFiles() { + File scheduledApkFile = new File(ContextUtils.getContext().getFilesDir(), SCHEDULED_APK_FILE); + Set<Path> paths = new ArraySet<>(); + if (!scheduledApkFile.exists()) { + return paths; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(scheduledApkFile)))) { + String line; + while ((line = reader.readLine()) != null) { + paths.add(Paths.get(line)); + } + } catch (IOException e) { + e.printStackTrace(); + } + return paths; + } + + private static void writeScheduledApkFiles(@NonNull Set<Path> paths) throws IOException { + File scheduledApkFile = new File(ContextUtils.getContext().getFilesDir(), SCHEDULED_APK_FILE); + try (PrintWriter writer = new PrintWriter(new FileOutputStream(scheduledApkFile))) { + for (Path path : paths) { + writer.println(path); + } + if (writer.checkError()) { + throw new IOException("Error occured while writing to the file"); + } + } + } +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModuleInfo.java b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModuleInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..8770ccb31c07be60ec5fb3fa89da9b705e1eaba4 --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskModuleInfo.java @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.AppManager.magisk; + +import android.content.Context; +import android.text.SpannableStringBuilder; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Properties; + +import io.github.muntashirakon.AppManager.R; +import io.github.muntashirakon.AppManager.utils.UIUtils; +import io.github.muntashirakon.io.Path; +import io.github.muntashirakon.util.LocalizedString; + +public class MagiskModuleInfo implements LocalizedString { + @NonNull + public static MagiskModuleInfo fromModule(@NonNull Path modulePath) throws IOException { + Properties prop = new Properties(); + try (InputStream is = modulePath.findFile(MagiskModule.MODULE_PROP).openInputStream()) { + prop.load(is); + } + return new MagiskModuleInfo( + prop.getProperty("id"), + prop.getProperty("name"), + prop.getProperty("version"), + Long.decode(prop.getProperty("versionCode")), + prop.getProperty("author"), + prop.getProperty("description"), + prop.getProperty("updateJson") + ); + } + + @NonNull + public final String id; + @NonNull + public final String name; + @NonNull + public final String version; + public final long versionCode; + @NonNull + public final String author; + @NonNull + public final String description; + @Nullable + public final String updateJson; + + public MagiskModuleInfo(@NonNull String id, @NonNull String name, @NonNull String version, long versionCode, + @NonNull String author, @NonNull String description, @Nullable String updateJson) { + this.id = id; + this.name = name; + this.version = version; + this.versionCode = versionCode; + this.author = author; + this.description = description; + this.updateJson = updateJson; + } + + @NonNull + @Override + public CharSequence toLocalizedString(@NonNull Context context) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(UIUtils.getStyledKeyValue(context, "ID", id)).append("\n") + .append(UIUtils.getStyledKeyValue(context, "Name", id)).append("\n") + .append(UIUtils.getStyledKeyValue(context, R.string.version, version + " (" + versionCode + ")")) + .append("\n") + .append(description); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MagiskModuleInfo)) return false; + MagiskModuleInfo that = (MagiskModuleInfo) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @NonNull + @Override + public String toString() { + return "MagiskModuleInfo{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", version='" + version + '\'' + + ", versionCode=" + versionCode + + '}'; + } +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskUtils.java index dc80686aa009bac754681f57f3455fb591f9c578..3e9bf49de5440faa7db6e17ecce47f9f65aee982 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskUtils.java @@ -9,7 +9,9 @@ import android.content.pm.ServiceInfo; import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -36,11 +38,6 @@ public class MagiskUtils { "/system/product/app", "/system/product/priv-app", "/system/product/overlay", "/system/vendor/app", "/system/vendor/overlay", "/system/system_ext/app", "/system/system_ext/priv-app", - "/system_ext/app", "/system_ext/priv-app", - - "/vendor/app", "/vendor/overlay", - - "/product/app", "/product/priv-app", "/product/overlay", }; @NonNull @@ -52,33 +49,57 @@ public class MagiskUtils { MagiskUtils.bootMode = bootMode; } - private static List<String> systemlessPaths; + private static HashMap<MagiskModuleInfo, List<String>> systemlessPaths; @NonNull - public static List<String> getSystemlessPaths() { + public static HashMap<MagiskModuleInfo, List<String>> getSystemlessPaths() { if (systemlessPaths == null) { - systemlessPaths = new ArrayList<>(); + systemlessPaths = new HashMap<>(); // Get module paths Path[] modulePaths = getModDir().listFiles(Path::isDirectory); // Scan module paths - for (Path file : modulePaths) { - // Get system apk files - for (String sysPath : SCAN_PATHS) { - // Always NonNull since it's a Linux FS - Path[] paths = Objects.requireNonNull(Paths.build(file, sysPath)).listFiles(Path::isDirectory); - for (Path path : paths) { - if (hasApkFile(path)) { - systemlessPaths.add(sysPath + "/" + path.getName()); + for (Path modulePath : modulePaths) { + try { + // Get module info + MagiskModuleInfo moduleInfo = MagiskModuleInfo.fromModule(modulePath); + // Get system apk files + for (String sysPath : SCAN_PATHS) { + // Always NonNull since it's a Linux FS + Path[] paths = Objects.requireNonNull(Paths.build(modulePath, sysPath)).listFiles(Path::isDirectory); + for (Path path : paths) { + if (hasApkFile(path)) { + List<String> addedPaths = systemlessPaths.get(moduleInfo); + if (addedPaths == null) { + addedPaths = new ArrayList<>(); + systemlessPaths.put(moduleInfo, addedPaths); + } + addedPaths.add(sysPath + "/" + path.getName()); + } } } + } catch (IOException e) { + e.printStackTrace(); } } } return systemlessPaths; } - public static boolean isSystemlessPath(String path) { - return getSystemlessPaths().contains(path); + @Nullable + public static MagiskModuleInfo getSystemlessPathInfo(@NonNull String path) { + String validPath = getValidSystemLocation(path); + if (validPath == null) { + // Invalid path + return null; + } + HashMap<MagiskModuleInfo, List<String>> systemlessPathInfo = getSystemlessPaths(); + for (MagiskModuleInfo moduleInfo : systemlessPathInfo.keySet()) { + List<String> systemlessPaths = systemlessPathInfo.get(moduleInfo); + if (systemlessPaths != null && systemlessPaths.contains(path)) { + return moduleInfo; + } + } + return null; } private static boolean hasApkFile(@NonNull Path file) { @@ -89,6 +110,24 @@ public class MagiskUtils { return false; } + @Nullable + public static String getValidSystemLocation(@NonNull String location) { + // We need to ensure that the paths are within the /system folder. + // Product, vendor and system_ext all have symlinks inside /system + if (location.startsWith("/product/") + || location.startsWith("/vendor/") + || location.startsWith("/system_ext/")) { + location = "/system" + location; + } + // Now, we need to check that the path is valid + for (String validPathPrefix : SCAN_PATHS) { + if (location.startsWith(validPathPrefix)) { + return location; + } + } + return null; + } + @NonNull static List<MagiskProcess> getProcesses(@NonNull PackageInfo packageInfo, @NonNull Collection<String> enabledProcesses) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/Prefs.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/Prefs.java index c1543031b7bcb0ed24b0b15b45bb28bd0d6eaeff..460c22e8fd61322d4c2c6569ca7230d02983a906 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/Prefs.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/Prefs.java @@ -26,6 +26,7 @@ import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.details.AppDetailsFragment; import io.github.muntashirakon.AppManager.fm.FmListOptions; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; +import io.github.muntashirakon.AppManager.magisk.MagiskUtils; import io.github.muntashirakon.AppManager.main.MainListOptions; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.runningapps.RunningAppsActivity; @@ -326,6 +327,11 @@ public final class Prefs { public static void setInstallerPackageName(@NonNull String packageName) { AppPref.set(AppPref.PrefKey.PREF_INSTALLER_INSTALLER_APP_STR, packageName); } + + public static boolean isSystemless() { + // TODO: 3/3/23 Also use Magisk prefs + return MagiskUtils.getModDir().exists(); + } } public static final class LogViewer { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/utils/PackageUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/utils/PackageUtils.java index 668dc8eace3ce9e6828cd546d09e8510e69672f2..5b7525670c4c33418b082ae3189adbe750a1e7eb 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/utils/PackageUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/utils/PackageUtils.java @@ -539,18 +539,25 @@ public final class PackageUtils { return sourceDir; } - public static String getHiddenCodePathOrDefault(String packageName, String defaultPath) { - Runner.Result result = Runner.runCommand(RunnerUtils.CMD_PM + " dump " + packageName + " | grep codePath"); + @Nullable + public static String getHiddenCodePathOrDefault(@NonNull ApplicationInfo applicationInfo) { + Runner.Result result = Runner.runCommand(RunnerUtils.CMD_PM + " dump " + applicationInfo.packageName + " | grep codePath"); if (result.isSuccessful()) { List<String> paths = result.getOutputAsList(); if (paths.size() > 0) { // Get only the last path String codePath = paths.get(paths.size() - 1); int start = codePath.indexOf('='); - if (start != -1) return codePath.substring(start + 1); + if (start != -1) return codePath.substring(start + 1).trim(); } } - return new File(defaultPath).getParent(); + if (applicationInfo.sourceDir == null) { + return null; + } + if (applicationInfo.packageName.equals("android")) { + return applicationInfo.sourceDir; + } + return new File(applicationInfo.sourceDir).getParent(); } @NonNull diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c286c29c199424878ff1726fa8661f5463372f42..0a421c5a4f19716e4a20dea82e3d1a3570ecab7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1331,6 +1331,7 @@ <string name="summary_perform_runtime_optimization_to_apps">Optimize DEX and (in Android 10 and later) layouts to improve the performance of the applications.</string> <string name="action_optimize_app">Optimize</string> <string name="batch_ops_runtime_optimization">Runtime optimization</string> + <string name="magisk_module_info_title">Magisk module info</string> <plurals name="alert_failed_to_optimize_apps"> <item quantity="one">Could not optimize %1$d app</item> <item quantity="other">Could not optimize %1$d apps</item>