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>