Logic to confirm permissions on install sessions.

When an app without INSTALL permission attempts to commit a session,
we involve user to confirm permissions.  We currently point at the
base APK, which defines all permissions for an app, handling the case
where a session may only be adding splits.

Add failure codes to represent rejection.  Fix bug by ignoring stages
during initial boot scan.

Bug: 16515814
Change-Id: I702bb72445216817bcc62b79c83980c1c2bb0120
diff --git a/api/current.txt b/api/current.txt
index 07974ed..e48abe2 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8680,6 +8680,7 @@
     field public static final int FAILURE_CONFLICT = 2; // 0x2
     field public static final int FAILURE_INCOMPATIBLE = 4; // 0x4
     field public static final int FAILURE_INVALID = 1; // 0x1
+    field public static final int FAILURE_REJECTED = 5; // 0x5
     field public static final int FAILURE_STORAGE = 3; // 0x3
     field public static final int FAILURE_UNKNOWN = 0; // 0x0
   }
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index cc0d569..5223476 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -36,4 +36,6 @@
 
     void uninstall(String packageName, int flags, in IPackageDeleteObserver2 observer, int userId);
     void uninstallSplit(String packageName, String splitName, int flags, in IPackageDeleteObserver2 observer, int userId);
+
+    void setPermissionsResult(int sessionId, boolean accepted);
 }
diff --git a/core/java/android/content/pm/InstallSessionInfo.java b/core/java/android/content/pm/InstallSessionInfo.java
index f263885..161bcde 100644
--- a/core/java/android/content/pm/InstallSessionInfo.java
+++ b/core/java/android/content/pm/InstallSessionInfo.java
@@ -16,7 +16,6 @@
 
 package android.content.pm;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -33,8 +32,12 @@
     /** {@hide} */
     public String installerPackageName;
     /** {@hide} */
+    public String resolvedBaseCodePath;
+    /** {@hide} */
     public float progress;
     /** {@hide} */
+    public boolean sealed;
+    /** {@hide} */
     public boolean open;
 
     /** {@hide} */
@@ -56,7 +59,9 @@
     public InstallSessionInfo(Parcel source) {
         sessionId = source.readInt();
         installerPackageName = source.readString();
+        resolvedBaseCodePath = source.readString();
         progress = source.readFloat();
+        sealed = source.readInt() != 0;
         open = source.readInt() != 0;
 
         mode = source.readInt();
@@ -149,7 +154,9 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(sessionId);
         dest.writeString(installerPackageName);
+        dest.writeString(resolvedBaseCodePath);
         dest.writeFloat(progress);
+        dest.writeInt(sealed ? 1 : 0);
         dest.writeInt(open ? 1 : 0);
 
         dest.writeInt(mode);
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 01c080d..525142b 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -81,6 +81,10 @@
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_SESSION_DETAILS = "android.content.pm.action.SESSION_DETAILS";
 
+    /** {@hide} */
+    public static final String
+            ACTION_CONFIRM_PERMISSIONS = "android.content.pm.action.CONFIRM_PERMISSIONS";
+
     /**
      * An integer session ID.
      *
@@ -206,6 +210,15 @@
         }
     }
 
+    /** {@hide} */
+    public void setPermissionsResult(int sessionId, boolean accepted) {
+        try {
+            mInstaller.setPermissionsResult(sessionId, accepted);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
     /**
      * Events for observing session lifecycle.
      * <p>
@@ -603,9 +616,8 @@
          * permission, incompatible certificates, etc. The user may be able to
          * uninstall another app to fix the issue.
          * <p>
-         * The extras bundle may contain {@link #EXTRA_PACKAGE_NAME} if one
-         * specific package was identified as the cause of the conflict. If
-         * unknown, or multiple packages, the extra may be {@code null}.
+         * The extras bundle may contain {@link #EXTRA_PACKAGE_NAME} with the
+         * specific packages identified as the cause of the conflict.
          */
         public static final int FAILURE_CONFLICT = 2;
 
@@ -626,6 +638,15 @@
          */
         public static final int FAILURE_INCOMPATIBLE = 4;
 
+        /**
+         * This install session failed because it was rejected. For example, the
+         * user declined requested permissions, or a package verifier rejected
+         * the session.
+         *
+         * @see PackageManager#VERIFICATION_REJECT
+         */
+        public static final int FAILURE_REJECTED = 5;
+
         public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
 
         /**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 1e4ed31..d5604cb 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -770,6 +770,9 @@
      */
     public static final int NO_NATIVE_LIBRARIES = -114;
 
+    /** {@hide} */
+    public static final int INSTALL_FAILED_REJECTED = -115;
+
     /**
      * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the
      * package's data directory.
@@ -3830,6 +3833,7 @@
             case INSTALL_FAILED_USER_RESTRICTED: return "INSTALL_FAILED_USER_RESTRICTED";
             case INSTALL_FAILED_DUPLICATE_PERMISSION: return "INSTALL_FAILED_DUPLICATE_PERMISSION";
             case INSTALL_FAILED_NO_MATCHING_ABIS: return "INSTALL_FAILED_NO_MATCHING_ABIS";
+            case INSTALL_FAILED_REJECTED: return "INSTALL_FAILED_REJECTED";
             default: return Integer.toString(status);
         }
     }
@@ -3857,8 +3861,8 @@
             case INSTALL_FAILED_CONTAINER_ERROR: return CommitCallback.FAILURE_STORAGE;
             case INSTALL_FAILED_INVALID_INSTALL_LOCATION: return CommitCallback.FAILURE_STORAGE;
             case INSTALL_FAILED_MEDIA_UNAVAILABLE: return CommitCallback.FAILURE_STORAGE;
-            case INSTALL_FAILED_VERIFICATION_TIMEOUT: return CommitCallback.FAILURE_UNKNOWN;
-            case INSTALL_FAILED_VERIFICATION_FAILURE: return CommitCallback.FAILURE_UNKNOWN;
+            case INSTALL_FAILED_VERIFICATION_TIMEOUT: return CommitCallback.FAILURE_REJECTED;
+            case INSTALL_FAILED_VERIFICATION_FAILURE: return CommitCallback.FAILURE_REJECTED;
             case INSTALL_FAILED_PACKAGE_CHANGED: return CommitCallback.FAILURE_INVALID;
             case INSTALL_FAILED_UID_CHANGED: return CommitCallback.FAILURE_INVALID;
             case INSTALL_FAILED_VERSION_DOWNGRADE: return CommitCallback.FAILURE_INVALID;
@@ -3876,6 +3880,7 @@
             case INSTALL_FAILED_USER_RESTRICTED: return CommitCallback.FAILURE_INCOMPATIBLE;
             case INSTALL_FAILED_DUPLICATE_PERMISSION: return CommitCallback.FAILURE_CONFLICT;
             case INSTALL_FAILED_NO_MATCHING_ABIS: return CommitCallback.FAILURE_INCOMPATIBLE;
+            case INSTALL_FAILED_REJECTED: return CommitCallback.FAILURE_REJECTED;
             default: return CommitCallback.FAILURE_UNKNOWN;
         }
     }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index b4faea1..5c77014 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -37,7 +37,6 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.Context;
-import android.content.pm.IPackageDeleteObserver;
 import android.content.pm.IPackageDeleteObserver2;
 import android.content.pm.IPackageInstaller;
 import android.content.pm.IPackageInstallerCallback;
@@ -199,6 +198,10 @@
         }
     }
 
+    public static boolean isStageFile(File file) {
+        return sStageFilter.accept(null, file.getName());
+    }
+
     @Deprecated
     public File allocateSessionDir() throws IOException {
         synchronized (mSessions) {
@@ -559,6 +562,15 @@
     }
 
     @Override
+    public void setPermissionsResult(int sessionId, boolean accepted) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, TAG);
+
+        synchronized (mSessions) {
+            mSessions.get(sessionId).setPermissionsResult(accepted);
+        }
+    }
+
+    @Override
     public void registerCallback(IPackageInstallerCallback callback, int userId) {
         mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, "registerCallback");
         enforceCallerCanReadSessions();
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 5443fbc..92bb44b 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
 import static android.content.pm.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED;
+import static android.content.pm.PackageManager.INSTALL_FAILED_REJECTED;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_WRONLY;
@@ -30,6 +31,7 @@
 import android.content.pm.IPackageInstallerSession;
 import android.content.pm.InstallSessionInfo;
 import android.content.pm.InstallSessionParams;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageParser;
 import android.content.pm.PackageParser.ApkLite;
@@ -106,10 +108,24 @@
     @GuardedBy("mLock")
     private boolean mSealed = false;
     @GuardedBy("mLock")
-    private boolean mPermissionsConfirmed = false;
+    private boolean mPermissionsAccepted = false;
     @GuardedBy("mLock")
     private boolean mDestroyed = false;
 
+    private int mFinalStatus;
+    private String mFinalMessage;
+
+    /**
+     * Path to the resolved base APK for this session, which may point at an APK
+     * inside the session (when the session defines the base), or it may point
+     * at the existing base APK (when adding splits to an existing app).
+     * <p>
+     * This is used when confirming permissions, since we can't fully stage the
+     * session inside an ASEC before confirming with user.
+     */
+    @GuardedBy("mLock")
+    private String mResolvedBaseCodePath;
+
     @GuardedBy("mLock")
     private ArrayList<FileBridge> mBridges = new ArrayList<>();
 
@@ -134,12 +150,7 @@
                 } catch (PackageManagerException e) {
                     Slog.e(TAG, "Install failed: " + e);
                     destroyInternal();
-                    try {
-                        mRemoteObserver.onPackageInstalled(mPackageName, e.error, e.getMessage(),
-                                null);
-                    } catch (RemoteException ignored) {
-                    }
-                    mCallback.onSessionFinished(PackageInstallerSession.this, false);
+                    dispatchSessionFinished(e.error, e.getMessage(), null);
                 }
 
                 return true;
@@ -169,9 +180,9 @@
 
         if (mPm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES,
                 installerPackageName) == PackageManager.PERMISSION_GRANTED) {
-            mPermissionsConfirmed = true;
+            mPermissionsAccepted = true;
         } else {
-            mPermissionsConfirmed = false;
+            mPermissionsAccepted = false;
         }
 
         computeProgressLocked();
@@ -182,7 +193,9 @@
 
         info.sessionId = sessionId;
         info.installerPackageName = installerPackageName;
+        info.resolvedBaseCodePath = mResolvedBaseCodePath;
         info.progress = mProgress;
+        info.sealed = mSealed;
         info.open = openCount.get() > 0;
 
         info.mode = params.mode;
@@ -355,11 +368,19 @@
 
         Preconditions.checkNotNull(mPackageName);
         Preconditions.checkNotNull(mSignatures);
+        Preconditions.checkNotNull(mResolvedBaseCodePath);
 
-        if (!mPermissionsConfirmed) {
-            // TODO: async confirm permissions with user
-            // when they confirm, we'll kick off another install() pass
-            throw new SecurityException("Caller must hold INSTALL permission");
+        if (!mPermissionsAccepted) {
+            // User needs to accept permissions; give installer an intent they
+            // can use to involve user.
+            final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS);
+            intent.setPackage("com.android.packageinstaller");
+            intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
+            try {
+                mRemoteObserver.onUserActionRequired(intent);
+            } catch (RemoteException ignored) {
+            }
+            return;
         }
 
         // Inherit any packages and native libraries from existing install that
@@ -386,12 +407,7 @@
             public void onPackageInstalled(String basePackageName, int returnCode, String msg,
                     Bundle extras) {
                 destroyInternal();
-                try {
-                    remoteObserver.onPackageInstalled(basePackageName, returnCode, msg, extras);
-                } catch (RemoteException ignored) {
-                }
-                final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED);
-                mCallback.onSessionFinished(PackageInstallerSession.this, success);
+                dispatchSessionFinished(returnCode, msg, extras);
             }
         };
 
@@ -409,6 +425,7 @@
         mPackageName = null;
         mVersionCode = -1;
         mSignatures = null;
+        mResolvedBaseCodePath = null;
 
         final File[] files = sessionStageDir.listFiles();
         if (ArrayUtils.isEmpty(files)) {
@@ -445,18 +462,25 @@
                     info.signatures);
 
             // Take this opportunity to enforce uniform naming
-            final String name;
+            final String targetName;
             if (info.splitName == null) {
-                name = "base.apk";
+                targetName = "base.apk";
             } else {
-                name = "split_" + info.splitName + ".apk";
+                targetName = "split_" + info.splitName + ".apk";
             }
-            if (!FileUtils.isValidExtFilename(name)) {
+            if (!FileUtils.isValidExtFilename(targetName)) {
                 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
-                        "Invalid filename: " + name);
+                        "Invalid filename: " + targetName);
             }
-            if (!file.getName().equals(name)) {
-                file.renameTo(new File(file.getParentFile(), name));
+
+            final File targetFile = new File(sessionStageDir, targetName);
+            if (!file.equals(targetFile)) {
+                file.renameTo(targetFile);
+            }
+
+            // Base is coming from session
+            if (info.splitName == null) {
+                mResolvedBaseCodePath = targetFile.getAbsolutePath();
             }
         }
 
@@ -472,13 +496,18 @@
             }
 
         } else {
-            // Partial installs must be consistent with existing install.
+            // Partial installs must be consistent with existing install
             final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId);
             if (app == null) {
                 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                         "Missing existing base package for " + mPackageName);
             }
 
+            // Base might be inherited from existing install
+            if (mResolvedBaseCodePath == null) {
+                mResolvedBaseCodePath = app.getBaseCodePath();
+            }
+
             final ApkLite info;
             try {
                 info = PackageParser.parseApkLite(new File(app.getBaseCodePath()),
@@ -537,6 +566,21 @@
         if (LOGD) Slog.d(TAG, "Spliced " + n + " existing APKs into stage");
     }
 
+    void setPermissionsResult(boolean accepted) {
+        if (!mSealed) {
+            throw new SecurityException("Must be sealed to accept permissions");
+        }
+
+        if (accepted) {
+            // Mark and kick off another install pass
+            mPermissionsAccepted = true;
+            mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
+        } else {
+            destroyInternal();
+            dispatchSessionFinished(INSTALL_FAILED_REJECTED, "User rejected permissions", null);
+        }
+    }
+
     @Override
     public void close() {
         if (openCount.decrementAndGet() == 0) {
@@ -546,11 +590,23 @@
 
     @Override
     public void abandon() {
-        try {
-            destroyInternal();
-        } finally {
-            mCallback.onSessionFinished(this, false);
+        destroyInternal();
+        dispatchSessionFinished(INSTALL_FAILED_INTERNAL_ERROR, "Session was abandoned", null);
+    }
+
+    private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
+        mFinalStatus = returnCode;
+        mFinalMessage = msg;
+
+        if (mRemoteObserver != null) {
+            try {
+                mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras);
+            } catch (RemoteException ignored) {
+            }
         }
+
+        final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED);
+        mCallback.onSessionFinished(this, success);
     }
 
     private void destroyInternal() {
@@ -578,9 +634,11 @@
         pw.printPair("mClientProgress", mClientProgress);
         pw.printPair("mProgress", mProgress);
         pw.printPair("mSealed", mSealed);
-        pw.printPair("mPermissionsConfirmed", mPermissionsConfirmed);
+        pw.printPair("mPermissionsAccepted", mPermissionsAccepted);
         pw.printPair("mDestroyed", mDestroyed);
         pw.printPair("mBridges", mBridges.size());
+        pw.printPair("mFinalStatus", mFinalStatus);
+        pw.printPair("mFinalMessage", mFinalMessage);
         pw.println();
 
         pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 4bf6636..6802fac 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4084,7 +4084,8 @@
         }
 
         for (File file : files) {
-            final boolean isPackage = isApkFile(file) || file.isDirectory();
+            final boolean isPackage = (isApkFile(file) || file.isDirectory())
+                    && !PackageInstallerService.isStageFile(file);
             if (!isPackage) {
                 // Ignore entries which are not apk's
                 continue;