Support non-privileged APKs in APEX.

This change adds initial support for scanning APEX directories looking
for APK, and make them available to the system as normal system APKs.
For now privileged apps and resource overlay are not supported, and the
support will be added in a later CL.

Scanning features are inherited from the partition in which the
preinstalled version of a given APEX module is located.

Supports both flattened and un-flattened APEX configurations.

Bug: 138429615
Test: Compiled a test apex with the Snake app installed in it. Verified
app is correctly scanned and usable. Tried also installing the apex in
/vendor and /product.

Change-Id: I8fb91317da0296567403b2b62babbb71e4b48938
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 6d5fe53b..a92237b 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -55,6 +55,7 @@
     private static final String ENV_VENDOR_ROOT = "VENDOR_ROOT";
     private static final String ENV_PRODUCT_ROOT = "PRODUCT_ROOT";
     private static final String ENV_SYSTEM_EXT_ROOT = "SYSTEM_EXT_ROOT";
+    private static final String ENV_APEX_ROOT = "APEX_ROOT";
 
     /** {@hide} */
     public static final String DIR_ANDROID = "Android";
@@ -78,7 +79,9 @@
     private static final File DIR_VENDOR_ROOT = getDirectory(ENV_VENDOR_ROOT, "/vendor");
     private static final File DIR_PRODUCT_ROOT = getDirectory(ENV_PRODUCT_ROOT, "/product");
     private static final File DIR_SYSTEM_EXT_ROOT = getDirectory(ENV_SYSTEM_EXT_ROOT,
-                                                           "/system_ext");
+            "/system_ext");
+    private static final File DIR_APEX_ROOT = getDirectory(ENV_APEX_ROOT,
+            "/apex");
 
     @UnsupportedAppUsage
     private static UserEnvironment sCurrentUser;
@@ -248,6 +251,16 @@
     }
 
     /**
+     * Return root directory of the apex mount point, where all the apex modules are made available
+     * to the rest of the system.
+     *
+     * @hide
+     */
+    public static @NonNull File getApexDirectory() {
+        return DIR_APEX_ROOT;
+    }
+
+    /**
      * Return the system directory for a user. This is for use by system
      * services to store files relating to the user. This directory will be
      * automatically deleted when the user is removed.
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index fd44cbb..12e8069 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageParser;
+import android.os.Environment;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.sysprop.ApexProperties;
@@ -47,6 +48,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -64,9 +66,9 @@
     static final int MATCH_FACTORY_PACKAGE = 1 << 1;
 
     /**
-     * Returns an instance of either {@link ApexManagerImpl} or {@link ApexManagerNoOp} depending
-     * on whenever this device supports APEX, i.e. {@link ApexProperties#updatable()} evaluates to
-     * {@code true}.
+     * Returns an instance of either {@link ApexManagerImpl} or {@link ApexManagerFlattenedApex}
+     * depending on whether this device supports APEX, i.e. {@link ApexProperties#updatable()}
+     * evaluates to {@code true}.
      */
     static ApexManager create(Context systemContext) {
         if (ApexProperties.updatable().orElse(false)) {
@@ -77,10 +79,28 @@
                 throw new IllegalStateException("Required service apexservice not available");
             }
         } else {
-            return new ApexManagerNoOp();
+            return new ApexManagerFlattenedApex();
         }
     }
 
+    /**
+     * Minimal information about APEX mount points and the original APEX package they refer to.
+     */
+    static class ActiveApexInfo {
+        public final File apexDirectory;
+        public final File preinstalledApexPath;
+
+        private ActiveApexInfo(File apexDirectory, File preinstalledApexPath) {
+            this.apexDirectory = apexDirectory;
+            this.preinstalledApexPath = preinstalledApexPath;
+        }
+    }
+
+    /**
+     * Returns {@link ActiveApexInfo} records relative to all active APEX packages.
+     */
+    abstract List<ActiveApexInfo> getActiveApexInfos();
+
     abstract void systemReady();
 
     /**
@@ -259,6 +279,22 @@
         }
 
         @Override
+        List<ActiveApexInfo> getActiveApexInfos() {
+            try {
+                return Arrays.stream(mApexService.getActivePackages())
+                        .map(apexInfo -> new ActiveApexInfo(
+                                new File(
+                                Environment.getApexDirectory() + File.separator
+                                        + apexInfo.moduleName),
+                                new File(apexInfo.modulePath))).collect(
+                                Collectors.toList());
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Unable to retrieve packages from apexservice", e);
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
         void systemReady() {
             mContext.registerReceiver(new BroadcastReceiver() {
                 @Override
@@ -549,7 +585,40 @@
      * An implementation of {@link ApexManager} that should be used in case device does not support
      * updating APEX packages.
      */
-    private static final class ApexManagerNoOp extends ApexManager {
+    private static final class ApexManagerFlattenedApex extends ApexManager {
+
+        @Override
+        List<ActiveApexInfo> getActiveApexInfos() {
+            // There is no apexd running in case of flattened apex
+            // We look up the /apex directory and identify the active APEX modules from there.
+            // As "preinstalled" path, we just report /system since in the case of flattened APEX
+            // the /apex directory is just a symlink to /system/apex.
+            List<ActiveApexInfo> result = new ArrayList<>();
+            File apexDir = Environment.getApexDirectory();
+            // In flattened configuration, init special-case the art directory and bind-mounts
+            // com.android.art.{release|debug} to com.android.art. At the time of writing, these
+            // directories are copied from the kArtApexDirNames variable in
+            // system/core/init/mount_namespace.cpp.
+            String[] skipDirs = {"com.android.art.release", "com.android.art.debug"};
+            if (apexDir.isDirectory()) {
+                File[] files = apexDir.listFiles();
+                // listFiles might be null if system server doesn't have permission to read
+                // a directory.
+                if (files != null) {
+                    for (File file : files) {
+                        if (file.isDirectory() && !file.getName().contains("@")) {
+                            for (String skipDir : skipDirs) {
+                                if (file.getName().equals(skipDir)) {
+                                    continue;
+                                }
+                            }
+                            result.add(new ActiveApexInfo(file, Environment.getRootDirectory()));
+                        }
+                    }
+                }
+            }
+            return result;
+        }
 
         @Override
         void systemReady() {
diff --git a/services/core/java/com/android/server/pm/PackageAbiHelperImpl.java b/services/core/java/com/android/server/pm/PackageAbiHelperImpl.java
index 1d3d24c..259200b 100644
--- a/services/core/java/com/android/server/pm/PackageAbiHelperImpl.java
+++ b/services/core/java/com/android/server/pm/PackageAbiHelperImpl.java
@@ -67,6 +67,15 @@
             codeRoot = Environment.getSystemExtDirectory();
         } else if (FileUtils.contains(Environment.getOdmDirectory(), codePath)) {
             codeRoot = Environment.getOdmDirectory();
+        } else if (FileUtils.contains(Environment.getApexDirectory(), codePath)) {
+            String fullPath = codePath.getAbsolutePath();
+            String[] parts = fullPath.split(File.separator);
+            if (parts.length > 2) {
+                codeRoot = new File(parts[1] + File.separator + parts[2]);
+            } else {
+                Slog.w(PackageManagerService.TAG, "Can't canonicalize code path " + codePath);
+                codeRoot = Environment.getApexDirectory();
+            }
         } else {
             // Unrecognized code path; take its top real segment as the apk root:
             // e.g. /something/app/blah.apk => /something
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index b5fa80d1..74a85d5 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -370,6 +370,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * Keep track of all those APKs everywhere.
@@ -762,6 +763,8 @@
                     new SystemPartition(Environment.getSystemExtDirectory(), SCAN_AS_SYSTEM_EXT,
                             true /* hasPriv */, true /* hasOverlays */)));
 
+    private final List<SystemPartition> mDirsToScanAsSystem;
+
     /**
      * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors.
      *
@@ -2552,6 +2555,16 @@
         mApexManager = ApexManager.create(mContext);
         mAppsFilter = mInjector.getAppsFilter();
 
+        mDirsToScanAsSystem = new ArrayList<>();
+        mDirsToScanAsSystem.addAll(SYSTEM_PARTITIONS);
+        mDirsToScanAsSystem.addAll(mApexManager.getActiveApexInfos().stream()
+                .map(ai -> resolveApexToSystemPartition(ai))
+                .filter(Objects::nonNull).collect(Collectors.toList()));
+        Slog.d(TAG,
+                "Directories scanned as system partitions: [" + mDirsToScanAsSystem.stream().map(
+                        d -> (d.folder.getAbsolutePath() + ":" + d.scanFlag))
+                        .collect(Collectors.joining(",")) + "]");
+
         // CHECKSTYLE:OFF IndentationCheck
         synchronized (mInstallLock) {
         // writer
@@ -2684,8 +2697,8 @@
             // reside in the right directory.
             final int systemParseFlags = mDefParseFlags | PackageParser.PARSE_IS_SYSTEM_DIR;
             final int systemScanFlags = scanFlags | SCAN_AS_SYSTEM;
-            for (int i = SYSTEM_PARTITIONS.size() - 1; i >= 0; i--) {
-                final SystemPartition partition = SYSTEM_PARTITIONS.get(i);
+            for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
+                final SystemPartition partition = mDirsToScanAsSystem.get(i);
                 if (partition.overlayFolder == null) {
                     continue;
                 }
@@ -2699,8 +2712,8 @@
                 throw new IllegalStateException(
                         "Failed to load frameworks package; check log for warnings");
             }
-            for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
-                final SystemPartition partition = SYSTEM_PARTITIONS.get(i);
+            for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
+                final SystemPartition partition = mDirsToScanAsSystem.get(i);
                 if (partition.privAppFolder != null) {
                     scanDirTracedLI(partition.privAppFolder, systemParseFlags,
                             systemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag, 0);
@@ -2878,8 +2891,8 @@
 
                         @ParseFlags int reparseFlags = 0;
                         @ScanFlags int rescanFlags = 0;
-                        for (int i1 = 0, size = SYSTEM_PARTITIONS.size(); i1 < size; i1++) {
-                            SystemPartition partition = SYSTEM_PARTITIONS.get(i1);
+                        for (int i1 = 0, size = mDirsToScanAsSystem.size(); i1 < size; i1++) {
+                            SystemPartition partition = mDirsToScanAsSystem.get(i1);
                             if (partition.containsPrivApp(scanFile)) {
                                 reparseFlags = systemParseFlags;
                                 rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED
@@ -17778,6 +17791,7 @@
     }
 
     static boolean locationIsPrivileged(String path) {
+        // TODO(dariofreni): include APEX partitions when they will support priv apps.
         for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
             SystemPartition partition = SYSTEM_PARTITIONS.get(i);
             if (partition.containsPrivPath(path)) {
@@ -17787,6 +17801,18 @@
         return false;
     }
 
+    private static @Nullable SystemPartition resolveApexToSystemPartition(
+            ApexManager.ActiveApexInfo apexInfo) {
+        for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
+            SystemPartition sp = SYSTEM_PARTITIONS.get(i);
+            if (apexInfo.preinstalledApexPath.getAbsolutePath().startsWith(
+                    sp.folder.getAbsolutePath())) {
+                return new SystemPartition(apexInfo.apexDirectory, sp.scanFlag,
+                        false /* hasPriv */, false /* hasOverlays */);
+            }
+        }
+        return null;
+    }
 
     /*
      * Tries to delete system package.
@@ -17897,8 +17923,8 @@
                 | PackageParser.PARSE_MUST_BE_APK
                 | PackageParser.PARSE_IS_SYSTEM_DIR;
         @ScanFlags int scanFlags = SCAN_AS_SYSTEM;
-        for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
-            SystemPartition partition = SYSTEM_PARTITIONS.get(i);
+        for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
+            SystemPartition partition = mDirsToScanAsSystem.get(i);
             if (partition.containsPath(codePathString)) {
                 scanFlags |= partition.scanFlag;
                 if (partition.containsPrivPath(codePathString)) {