Factor out calls to apexservice in a class.

The ApexManager class provides a cleaner interface to the apex service,
as well as providing caching for active packages, which can't change on
a running system. The cache is populated at boot time.

This CL will also cause PackageManager to stop reporting APEX packages
on devices that ship with flattened APEXs.

Test: atest apex_e2e_tests; used small app to verify API calls still
work; checked output of dumpsys.
Test: checked that on marlin (target with flatten APEX) no APEXs are
reported and no crashes are experienced at boot.
Fix: 123052859
Fix: 122638509
Fix: 124299505
Bug: 122952270
Change-Id: Iefe4fb42e455a7479ff47eb776d3492de8395469
(cherry picked from commit 2e8dffcb726bf30e511047c6685fcd8a5442c281)
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
new file mode 100644
index 0000000..dac4b6f
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.s
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.apex.ApexInfo;
+import android.apex.ApexInfoList;
+import android.apex.ApexSessionInfo;
+import android.apex.IApexService;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageParserException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * ApexManager class handles communications with the apex service to perform operation and queries,
+ * as well as providing caching to avoid unnecessary calls to the service.
+ */
+class ApexManager {
+    static final String TAG = "ApexManager";
+    private final IApexService mApexService;
+    private final Map<String, PackageInfo> mActivePackagesCache;
+
+    ApexManager() {
+        mApexService = IApexService.Stub.asInterface(
+            ServiceManager.getService("apexservice"));
+        mActivePackagesCache = populateActivePackagesCache();
+    }
+
+    @NonNull
+    private Map<String, PackageInfo> populateActivePackagesCache() {
+        try {
+            List<PackageInfo> list = new ArrayList<>();
+            final ApexInfo[] activePkgs = mApexService.getActivePackages();
+            for (ApexInfo ai : activePkgs) {
+                // If the device is using flattened APEX, don't report any APEX
+                // packages since they won't be managed or updated by PackageManager.
+                if ((new File(ai.packagePath)).isDirectory()) {
+                    break;
+                }
+                try {
+                    list.add(PackageParser.generatePackageInfoFromApex(
+                            new File(ai.packagePath), true /* collect certs */));
+                } catch (PackageParserException pe) {
+                    throw new IllegalStateException("Unable to parse: " + ai, pe);
+                }
+            }
+            return list.stream().collect(Collectors.toMap(p -> p.packageName, Function.identity()));
+        } catch (RemoteException re) {
+            Slog.e(TAG, "Unable to retrieve packages from apexservice: " + re.toString());
+            throw new RuntimeException(re);
+        }
+    }
+
+    /**
+     * Retrieves information about an active APEX package.
+     *
+     * @param packageName the package name to look for. Note that this is the package name reported
+     *                    in the APK container manifest (i.e. AndroidManifest.xml), which might
+     *                    differ from the one reported in the APEX manifest (i.e.
+     *                    apex_manifest.json).
+     * @return a PackageInfo object with the information about the package, or null if the package
+     *         is not found.
+     */
+    @Nullable PackageInfo getActivePackage(String packageName) {
+        return mActivePackagesCache.get(packageName);
+    }
+
+    /**
+     * Retrieves information about all active APEX packages.
+     *
+     * @return a Collection of PackageInfo object, each one containing information about a different
+     *         active package.
+     */
+    Collection<PackageInfo> getActivePackages() {
+        return mActivePackagesCache.values();
+    }
+
+    /**
+     * Retrieves information about an apexd staged session i.e. the internal state used by apexd to
+     * track the different states of a session.
+     *
+     * @param sessionId the identifier of the session.
+     * @return an ApexSessionInfo object, or null if the session is not known.
+     */
+    @Nullable ApexSessionInfo getStagedSessionInfo(int sessionId) {
+        try {
+            ApexSessionInfo apexSessionInfo = mApexService.getStagedSessionInfo(sessionId);
+            if (apexSessionInfo.isUnknown) {
+                return null;
+            }
+            return apexSessionInfo;
+        } catch (RemoteException re) {
+            Slog.e(TAG, "Unable to contact apexservice", re);
+            throw new RuntimeException(re);
+        }
+    }
+
+    /**
+     * Submit a staged session to apex service. This causes the apex service to perform some initial
+     * verification and accept or reject the session. Submitting a session successfully is not
+     * enough for it to be activated at the next boot, the caller needs to call
+     * {@link #markStagedSessionReady(int)}.
+     *
+     * @param sessionId the identifier of the {@link PackageInstallerSession} being submitted.
+     * @param childSessionIds if {@code sessionId} is a multi-package session, this should contain
+     *                        an array of identifiers of all the child sessions. Otherwise it should
+     *                        be an empty array.
+     * @param apexInfoList this is an output parameter, which needs to be initialized by tha caller
+     *                     and will be filled with a list of {@link ApexInfo} objects, each of which
+     *                     contains metadata about one of the packages being submitted as part of
+     *                     the session.
+     * @return whether the submission of the session was successful.
+     */
+    boolean submitStagedSession(
+            int sessionId, @NonNull int[] childSessionIds, @NonNull ApexInfoList apexInfoList) {
+        try {
+            return mApexService.submitStagedSession(sessionId, childSessionIds, apexInfoList);
+        } catch (RemoteException re) {
+            Slog.e(TAG, "Unable to contact apexservice", re);
+            throw new RuntimeException(re);
+        }
+    }
+
+    /**
+     * Mark a staged session previously submitted using {@cde submitStagedSession} as ready to be
+     * applied at next reboot.
+     *
+     * @param sessionId the identifier of the {@link PackageInstallerSession} being marked as ready.
+     * @return true upon success, false if the session is unknown.
+     */
+    boolean markStagedSessionReady(int sessionId) {
+        try {
+            return mApexService.markStagedSessionReady(sessionId);
+        } catch (RemoteException re) {
+            Slog.e(TAG, "Unable to contact apexservice", re);
+            throw new RuntimeException(re);
+        }
+    }
+
+    /**
+     * Dumps various state information to the provided {@link PrintWriter} object.
+     *
+     * @param pw the {@link PrintWriter} object to send information to.
+     * @param packageName a {@link String} containing a package name, or {@code null}. If set, only
+     *                    information about that specific package will be dumped.
+     */
+    void dump(PrintWriter pw, @Nullable String packageName) {
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
+        ipw.println();
+        ipw.println("Active APEX packages:");
+        ipw.increaseIndent();
+        try {
+            populateActivePackagesCache();
+            for (PackageInfo pi : mActivePackagesCache.values()) {
+                if (packageName != null && !packageName.equals(pi.packageName)) {
+                    continue;
+                }
+                ipw.println(pi.packageName);
+                ipw.increaseIndent();
+                ipw.println("Version: " + pi.versionCode);
+                ipw.println("Path: " + pi.applicationInfo.sourceDir);
+                ipw.decreaseIndent();
+            }
+            ipw.decreaseIndent();
+            ipw.println();
+            ipw.println("APEX session state:");
+            ipw.increaseIndent();
+            final ApexSessionInfo[] sessions = mApexService.getSessions();
+            for (ApexSessionInfo si : sessions) {
+                ipw.println("Session ID: " + Integer.toString(si.sessionId));
+                ipw.increaseIndent();
+                if (si.isUnknown) {
+                    ipw.println("State: UNKNOWN");
+                } else if (si.isVerified) {
+                    ipw.println("State: VERIFIED");
+                } else if (si.isStaged) {
+                    ipw.println("State: STAGED");
+                } else if (si.isActivated) {
+                    ipw.println("State: ACTIVATED");
+                } else if (si.isActivationPendingRetry) {
+                    ipw.println("State: ACTIVATION PENDING RETRY");
+                } else if (si.isActivationFailed) {
+                    ipw.println("State: ACTIVATION FAILED");
+                }
+                ipw.decreaseIndent();
+            }
+            ipw.decreaseIndent();
+        } catch (RemoteException e) {
+            ipw.println("Couldn't communicate with apexd.");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 21965e4..75ab6c5 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -186,7 +186,7 @@
         }
     };
 
-    public PackageInstallerService(Context context, PackageManagerService pm) {
+    public PackageInstallerService(Context context, PackageManagerService pm, ApexManager am) {
         mContext = context;
         mPm = pm;
         mPermissionManager = LocalServices.getService(PermissionManagerInternal.class);
@@ -204,7 +204,7 @@
         mSessionsDir = new File(Environment.getDataSystemDirectory(), "install_sessions");
         mSessionsDir.mkdirs();
 
-        mStagingManager = new StagingManager(pm, this);
+        mStagingManager = new StagingManager(pm, this, am);
     }
 
     private void setBootCompleted()  {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 874d1a7..a6bcb53 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -119,9 +119,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.apex.ApexInfo;
-import android.apex.ApexSessionInfo;
-import android.apex.IApexService;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.AppDetailsActivity;
@@ -733,10 +730,10 @@
     @GuardedBy("mPackages")
     final private ArraySet<PackageListObserver> mPackageListObservers = new ArraySet<>();
 
-    private PackageManager mPackageManager;
-
     private final ModuleInfoProvider mModuleInfoProvider;
 
+    private final ApexManager mApexManager;
+
     class PackageParserCallback implements PackageParser.Callback {
         @Override public final boolean hasFeature(String feature) {
             return PackageManagerService.this.hasSystemFeature(feature, 0);
@@ -3074,7 +3071,8 @@
                 }
             }
 
-            mInstallerService = new PackageInstallerService(context, this);
+            mApexManager = new ApexManager();
+            mInstallerService = new PackageInstallerService(context, this, mApexManager);
             final Pair<ComponentName, String> instantAppResolverComponent =
                     getInstantAppResolverLPr();
             if (instantAppResolverComponent != null) {
@@ -3934,27 +3932,7 @@
             }
             //
             if (!matchFactoryOnly && (flags & MATCH_APEX) != 0) {
-                //TODO(b/123052859) Don't do file operations every time there is a query.
-                final IApexService apex = IApexService.Stub.asInterface(
-                        ServiceManager.getService("apexservice"));
-                if (apex != null) {
-                    try {
-                        final ApexInfo activePkg = apex.getActivePackage(packageName);
-                        if (activePkg != null && !TextUtils.isEmpty(activePkg.packagePath)) {
-                            try {
-                                return PackageParser.generatePackageInfoFromApex(
-                                        new File(activePkg.packagePath), true /* collect certs */);
-                            } catch (PackageParserException pe) {
-                                Log.e(TAG, "Unable to parse package at "
-                                        + activePkg.packagePath, pe);
-                            }
-                        }
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Unable to retrieve packages from apexservice: " + e.toString());
-                    }
-                } else {
-                    Log.e(TAG, "Unable to connect to apexservice for querying packages.");
-                }
+                return mApexManager.getActivePackage(packageName);
             }
         }
         return null;
@@ -7851,25 +7829,7 @@
             if (listApex) {
                 // TODO(b/119767311): include uninstalled/inactive APEX if
                 //  MATCH_UNINSTALLED_PACKAGES is set.
-                final IApexService apex = IApexService.Stub.asInterface(
-                        ServiceManager.getService("apexservice"));
-                if (apex != null) {
-                    try {
-                        final ApexInfo[] activePkgs = apex.getActivePackages();
-                        for (ApexInfo ai : activePkgs) {
-                            try {
-                                 list.add(PackageParser.generatePackageInfoFromApex(
-                                         new File(ai.packagePath), true /* collect certs */));
-                            } catch (PackageParserException pe) {
-                                 throw new IllegalStateException("Unable to parse: " + ai, pe);
-                            }
-                        }
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Unable to retrieve packages from apexservice: " + e.toString());
-                    }
-                } else {
-                    Log.e(TAG, "Unable to connect to apexservice for querying packages.");
-                }
+                list.addAll(mApexManager.getActivePackages());
             }
             return new ParceledListSlice<>(list);
         }
@@ -21319,51 +21279,7 @@
         }
 
         if (!checkin && dumpState.isDumping(DumpState.DUMP_APEX)) {
-            final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
-            ipw.println();
-            ipw.println("Active APEX packages:");
-            ipw.increaseIndent();
-            final IApexService apex = IApexService.Stub.asInterface(
-                    ServiceManager.getService("apexservice"));
-            try {
-                final ApexInfo[] activeApexes = apex.getActivePackages();
-                for (ApexInfo ai : activeApexes) {
-                    if (packageName != null && !packageName.equals(ai.packageName)) {
-                        continue;
-                    }
-                    ipw.println(ai.packageName);
-                    ipw.increaseIndent();
-                    ipw.println("Version: " + Long.toString(ai.versionCode));
-                    ipw.println("Path: " + ai.packagePath);
-                    ipw.decreaseIndent();
-                }
-                ipw.decreaseIndent();
-                ipw.println();
-                ipw.println("APEX session state:");
-                ipw.increaseIndent();
-                final ApexSessionInfo[] sessions = apex.getSessions();
-                for (ApexSessionInfo si : sessions) {
-                    ipw.println("Session ID: " + Integer.toString(si.sessionId));
-                    ipw.increaseIndent();
-                    if (si.isUnknown) {
-                        ipw.println("State: UNKNOWN");
-                    } else if (si.isVerified) {
-                        ipw.println("State: VERIFIED");
-                    } else if (si.isStaged) {
-                        ipw.println("State: STAGED");
-                    } else if (si.isActivated) {
-                        ipw.println("State: ACTIVATED");
-                    } else if (si.isActivationPendingRetry) {
-                        ipw.println("State: ACTIVATION PENDING RETRY");
-                    } else if (si.isActivationFailed) {
-                        ipw.println("State: ACTIVATION FAILED");
-                    }
-                    ipw.decreaseIndent();
-                }
-                ipw.decreaseIndent();
-            } catch (RemoteException e) {
-                ipw.println("Couldn't communicate with apexd.");
-            }
+            mApexManager.dump(pw, packageName);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java
index fa8360b..30c2281 100644
--- a/services/core/java/com/android/server/pm/StagingManager.java
+++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -20,12 +20,12 @@
 import android.apex.ApexInfo;
 import android.apex.ApexInfoList;
 import android.apex.ApexSessionInfo;
-import android.apex.IApexService;
 import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
@@ -41,7 +41,6 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.text.TextUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.apk.ApkSignatureVerifier;
@@ -68,14 +67,16 @@
 
     private final PackageInstallerService mPi;
     private final PackageManagerService mPm;
+    private final ApexManager mApexManager;
     private final Handler mBgHandler;
 
     @GuardedBy("mStagedSessions")
     private final SparseArray<PackageInstallerSession> mStagedSessions = new SparseArray<>();
 
-    StagingManager(PackageManagerService pm, PackageInstallerService pi) {
+    StagingManager(PackageManagerService pm, PackageInstallerService pi, ApexManager am) {
         mPm = pm;
         mPi = pi;
+        mApexManager = am;
         mBgHandler = BackgroundThread.getHandler();
     }
 
@@ -100,7 +101,7 @@
         return new ParceledListSlice<>(result);
     }
 
-    private static boolean validateApexSignature(String apexPath, String packageName) {
+    private boolean validateApexSignature(String apexPath, String packageName) {
         final SigningDetails signingDetails;
         try {
             signingDetails = ApkSignatureVerifier.verify(apexPath, SignatureSchemeVersion.JAR);
@@ -109,17 +110,9 @@
             return false;
         }
 
-        final IApexService apex = IApexService.Stub.asInterface(
-                ServiceManager.getService("apexservice"));
-        final ApexInfo apexInfo;
-        try {
-            apexInfo = apex.getActivePackage(packageName);
-        } catch (RemoteException re) {
-            Slog.e(TAG, "Unable to contact APEXD", re);
-            return false;
-        }
+        final PackageInfo packageInfo = mApexManager.getActivePackage(packageName);
 
-        if (apexInfo == null || TextUtils.isEmpty(apexInfo.packageName)) {
+        if (packageInfo == null) {
             // TODO: What is the right thing to do here ? This implies there's no active package
             // with the given name. This should never be the case in production (where we only
             // accept updates to existing APEXes) but may be required for testing.
@@ -129,9 +122,10 @@
         final SigningDetails existingSigningDetails;
         try {
             existingSigningDetails = ApkSignatureVerifier.verify(
-                apexInfo.packagePath, SignatureSchemeVersion.JAR);
+                packageInfo.applicationInfo.sourceDir, SignatureSchemeVersion.JAR);
         } catch (PackageParserException e) {
-            Slog.e(TAG, "Unable to parse APEX package: " + apexInfo.packagePath, e);
+            Slog.e(TAG, "Unable to parse APEX package: "
+                    + packageInfo.applicationInfo.sourceDir, e);
             return false;
         }
 
@@ -143,10 +137,10 @@
         return false;
     }
 
-    private static boolean submitSessionToApexService(@NonNull PackageInstallerSession session,
-                                                      List<PackageInstallerSession> childSessions,
-                                                      ApexInfoList apexInfoList) {
-        return sendSubmitStagedSessionRequest(
+    private boolean submitSessionToApexService(@NonNull PackageInstallerSession session,
+                                               List<PackageInstallerSession> childSessions,
+                                               ApexInfoList apexInfoList) {
+        return mApexManager.submitStagedSession(
                 session.sessionId,
                 childSessions != null
                         ? childSessions.stream().mapToInt(s -> s.sessionId).toArray() :
@@ -154,33 +148,6 @@
                 apexInfoList);
     }
 
-    private static boolean sendSubmitStagedSessionRequest(
-            int sessionId, int[] childSessionIds, ApexInfoList apexInfoList) {
-        final IApexService apex = IApexService.Stub.asInterface(
-                ServiceManager.getService("apexservice"));
-        boolean success;
-        try {
-            success = apex.submitStagedSession(sessionId, childSessionIds, apexInfoList);
-        } catch (RemoteException re) {
-            Slog.e(TAG, "Unable to contact apexservice", re);
-            return false;
-        }
-        return success;
-    }
-
-    private static boolean sendMarkStagedSessionReadyRequest(int sessionId) {
-        final IApexService apex = IApexService.Stub.asInterface(
-                ServiceManager.getService("apexservice"));
-        boolean success;
-        try {
-            success = apex.markStagedSessionReady(sessionId);
-        } catch (RemoteException re) {
-            Slog.e(TAG, "Unable to contact apexservice", re);
-            return false;
-        }
-        return success;
-    }
-
     private static boolean isApexSession(@NonNull PackageInstallerSession session) {
         return (session.params.installFlags & PackageManager.INSTALL_APEX) != 0;
     }
@@ -260,7 +227,7 @@
         }
 
         session.setStagedSessionReady();
-        if (!sendMarkStagedSessionReadyRequest(session.sessionId)) {
+        if (!mApexManager.markStagedSessionReady(session.sessionId)) {
             session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
                             "APEX staging failed, check logcat messages from apexd for more "
                             + "details.");
@@ -284,16 +251,12 @@
 
     private void resumeSession(@NonNull PackageInstallerSession session) {
         if (sessionContainsApex(session)) {
-            // Check with apexservice whether the apex
-            // packages have been activated.
-            final IApexService apex = IApexService.Stub.asInterface(
-                    ServiceManager.getService("apexservice"));
-            ApexSessionInfo apexSessionInfo;
-            try {
-                apexSessionInfo = apex.getStagedSessionInfo(session.sessionId);
-            } catch (RemoteException re) {
-                Slog.e(TAG, "Unable to contact apexservice", re);
-                // TODO should we retry here? Mark the session as failed?
+            // Check with apexservice whether the apex packages have been activated.
+            ApexSessionInfo apexSessionInfo = mApexManager.getStagedSessionInfo(session.sessionId);
+            if (apexSessionInfo == null) {
+                session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
+                        "apexd did not know anything about a staged session supposed to be"
+                        + "activated");
                 return;
             }
             if (apexSessionInfo.isActivationFailed || apexSessionInfo.isUnknown) {
@@ -323,8 +286,8 @@
         // The APEX part of the session is activated, proceed with the installation of APKs.
         if (!installApksInSession(session)) {
             session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
-                    "APEX activation failed. Check logcat messages from apexd for "
-                            + "more information.");
+                    "Staged installation of APKs failed. Check logcat messages for"
+                        + "more information.");
             return;
         }
         session.setStagedSessionApplied();