Allow to transfer+seal a install session

... so that one package can supply the data and another one can issue
the commit.

Also allow reading of sealed sessions.

Also lock more in PackageInstallerSession so that we can be sure the
session is not used by the old package anymore once transferred and that
all calls into the session work on consistent data.

Bug: 37281396
Test: cts-tradefed run cts-dev -m CtsContentTestCases --test=android.content.pm.cts.InstallSessionTransferTest
      Installed and uninstalled packages via the PackageInstaller app
      Installed and uninstalled packages via the Google Play Store

Change-Id: Id4b7a0071d703b7d18c9f5bf2bd15ebf67086d07
diff --git a/api/current.txt b/api/current.txt
index 8d2e9c8..5f17b8a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10506,6 +10506,7 @@
     method public java.io.OutputStream openWrite(java.lang.String, long, long) throws java.io.IOException;
     method public void removeSplit(java.lang.String) throws java.io.IOException;
     method public void setStagingProgress(float);
+    method public void transfer(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public static abstract class PackageInstaller.SessionCallback {
diff --git a/api/system-current.txt b/api/system-current.txt
index e2754fd..cd8d054 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -11180,12 +11180,14 @@
     method public void abandon();
     method public void close();
     method public void commit(android.content.IntentSender);
+    method public void commitTransferred(android.content.IntentSender);
     method public void fsync(java.io.OutputStream) throws java.io.IOException;
     method public java.lang.String[] getNames() throws java.io.IOException;
     method public java.io.InputStream openRead(java.lang.String) throws java.io.IOException;
     method public java.io.OutputStream openWrite(java.lang.String, long, long) throws java.io.IOException;
     method public void removeSplit(java.lang.String) throws java.io.IOException;
     method public void setStagingProgress(float);
+    method public void transfer(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public static abstract class PackageInstaller.SessionCallback {
diff --git a/api/test-current.txt b/api/test-current.txt
index fcb404e..f846205 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -10544,6 +10544,7 @@
     method public java.io.OutputStream openWrite(java.lang.String, long, long) throws java.io.IOException;
     method public void removeSplit(java.lang.String) throws java.io.IOException;
     method public void setStagingProgress(float);
+    method public void transfer(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public static abstract class PackageInstaller.SessionCallback {
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index 2a3fac3..0b16852 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -32,6 +32,7 @@
     void removeSplit(String splitName);
 
     void close();
-    void commit(in IntentSender statusReceiver);
+    void commit(in IntentSender statusReceiver, boolean forTransferred);
+    void transfer(in String packageName);
     void abandon();
 }
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index c3ebf55..38b3487 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -38,6 +38,7 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.os.ParcelableException;
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.system.ErrnoException;
@@ -793,7 +794,7 @@
          * @throws IOException if trouble opening the file for writing, such as
          *             lack of disk space or unavailable media.
          * @throws SecurityException if called after the session has been
-         *             committed or abandoned.
+         *             sealed or abandoned
          */
         public @NonNull OutputStream openWrite(@NonNull String name, long offsetBytes,
                 long lengthBytes) throws IOException {
@@ -918,7 +919,68 @@
          */
         public void commit(@NonNull IntentSender statusReceiver) {
             try {
-                mSession.commit(statusReceiver);
+                mSession.commit(statusReceiver, false);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Attempt to commit a session that has been {@link #transfer(String) transferred}.
+         *
+         * <p>If the device reboots before the session has been finalized, you may commit the
+         * session again.
+         *
+         * <p>The caller of this method is responsible to ensure the safety of the session. As the
+         * session was created by another - usually less trusted - app, it is paramount that before
+         * committing <u>all</u> public and system {@link SessionInfo properties of the session}
+         * and <u>all</u> {@link #openRead(String) APKs} are verified by the caller. It might happen
+         * that new properties are added to the session with a new API revision. In this case the
+         * callers need to be updated.
+         *
+         * @param statusReceiver Callbacks called when the state of the session changes.
+         *
+         * @hide
+         */
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES)
+        public void commitTransferred(@NonNull IntentSender statusReceiver) {
+            try {
+                mSession.commit(statusReceiver, true);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Transfer the session to a new owner.
+         * <p>
+         * Only sessions that update the installing app can be transferred.
+         * <p>
+         * After the transfer to a package with a different uid all method calls on the session
+         * will cause {@link SecurityException}s.
+         * <p>
+         * Once this method is called, the session is sealed and no additional mutations beside
+         * committing it may be performed on the session.
+         *
+         * @param packageName The package of the new owner. Needs to hold the INSTALL_PACKAGES
+         *                    permission.
+         *
+         * @throws PackageManager.NameNotFoundException if the new owner could not be found.
+         * @throws SecurityException if called after the session has been committed or abandoned.
+         * @throws SecurityException if the session does not update the original installer
+         * @throws SecurityException if streams opened through
+         *                           {@link #openWrite(String, long, long) are still open.
+         */
+        public void transfer(@NonNull String packageName)
+                throws PackageManager.NameNotFoundException {
+            Preconditions.checkNotNull(packageName);
+
+            try {
+                mSession.transfer(packageName);
+            } catch (ParcelableException e) {
+                e.maybeRethrow(PackageManager.NameNotFoundException.class);
+                throw new RuntimeException(e);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index bab7011..c3b93b4 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -16,17 +16,6 @@
 
 package com.android.server.pm;
 
-import static com.android.internal.util.XmlUtils.readBitmapAttribute;
-import static com.android.internal.util.XmlUtils.readBooleanAttribute;
-import static com.android.internal.util.XmlUtils.readIntAttribute;
-import static com.android.internal.util.XmlUtils.readLongAttribute;
-import static com.android.internal.util.XmlUtils.readStringAttribute;
-import static com.android.internal.util.XmlUtils.readUriAttribute;
-import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
-import static com.android.internal.util.XmlUtils.writeIntAttribute;
-import static com.android.internal.util.XmlUtils.writeLongAttribute;
-import static com.android.internal.util.XmlUtils.writeStringAttribute;
-import static com.android.internal.util.XmlUtils.writeUriAttribute;
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
@@ -54,8 +43,6 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.VersionedPackage;
 import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -84,9 +71,6 @@
 import android.util.SparseIntArray;
 import android.util.Xml;
 
-import java.io.CharArrayWriter;
-import libcore.io.IoUtils;
-
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.content.PackageHelper;
@@ -97,10 +81,13 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.IoThread;
 
+import libcore.io.IoUtils;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import java.io.CharArrayWriter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -125,32 +112,6 @@
 
     /** XML constants used in {@link #mSessionsFile} */
     private static final String TAG_SESSIONS = "sessions";
-    private static final String TAG_SESSION = "session";
-    private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission";
-    private static final String ATTR_SESSION_ID = "sessionId";
-    private static final String ATTR_USER_ID = "userId";
-    private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName";
-    private static final String ATTR_INSTALLER_UID = "installerUid";
-    private static final String ATTR_CREATED_MILLIS = "createdMillis";
-    private static final String ATTR_SESSION_STAGE_DIR = "sessionStageDir";
-    private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid";
-    private static final String ATTR_PREPARED = "prepared";
-    private static final String ATTR_SEALED = "sealed";
-    private static final String ATTR_MODE = "mode";
-    private static final String ATTR_INSTALL_FLAGS = "installFlags";
-    private static final String ATTR_INSTALL_LOCATION = "installLocation";
-    private static final String ATTR_SIZE_BYTES = "sizeBytes";
-    private static final String ATTR_APP_PACKAGE_NAME = "appPackageName";
-    @Deprecated
-    private static final String ATTR_APP_ICON = "appIcon";
-    private static final String ATTR_APP_LABEL = "appLabel";
-    private static final String ATTR_ORIGINATING_URI = "originatingUri";
-    private static final String ATTR_ORIGINATING_UID = "originatingUid";
-    private static final String ATTR_REFERRER_URI = "referrerUri";
-    private static final String ATTR_ABI_OVERRIDE = "abiOverride";
-    private static final String ATTR_VOLUME_UUID = "volumeUuid";
-    private static final String ATTR_NAME = "name";
-    private static final String ATTR_INSTALL_REASON = "installRason";
 
     /** Automatically destroy sessions older than this */
     private static final long MAX_AGE_MILLIS = 3 * DateUtils.DAY_IN_MILLIS;
@@ -357,8 +318,10 @@
             while ((type = in.next()) != END_DOCUMENT) {
                 if (type == START_TAG) {
                     final String tag = in.getName();
-                    if (TAG_SESSION.equals(tag)) {
-                        final PackageInstallerSession session = readSessionLocked(in);
+                    if (PackageInstallerSession.TAG_SESSION.equals(tag)) {
+                        final PackageInstallerSession session = PackageInstallerSession.
+                                readFromXml(in, mInternalCallback, mContext, mPm,
+                                        mInstallThread.getLooper(), mSessionsDir);
                         final long age = System.currentTimeMillis() - session.createdMillis;
 
                         final boolean valid;
@@ -397,53 +360,10 @@
         session.dump(pw);
         mHistoricalSessions.add(writer.toString());
 
+        int installerUid = session.getInstallerUid();
         // Increment the number of sessions by this installerUid.
-        mHistoricalSessionsByInstaller.put(
-                session.installerUid,
-                mHistoricalSessionsByInstaller.get(session.installerUid) + 1);
-    }
-
-    private PackageInstallerSession readSessionLocked(XmlPullParser in) throws IOException,
-            XmlPullParserException {
-        final int sessionId = readIntAttribute(in, ATTR_SESSION_ID);
-        final int userId = readIntAttribute(in, ATTR_USER_ID);
-        final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME);
-        final int installerUid = readIntAttribute(in, ATTR_INSTALLER_UID, mPm.getPackageUid(
-                installerPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId));
-        final long createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS);
-        final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR);
-        final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null;
-        final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID);
-        final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true);
-        final boolean sealed = readBooleanAttribute(in, ATTR_SEALED);
-
-        final SessionParams params = new SessionParams(
-                SessionParams.MODE_INVALID);
-        params.mode = readIntAttribute(in, ATTR_MODE);
-        params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS);
-        params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION);
-        params.sizeBytes = readLongAttribute(in, ATTR_SIZE_BYTES);
-        params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME);
-        params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON);
-        params.appLabel = readStringAttribute(in, ATTR_APP_LABEL);
-        params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI);
-        params.originatingUid =
-                readIntAttribute(in, ATTR_ORIGINATING_UID, SessionParams.UID_UNKNOWN);
-        params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI);
-        params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE);
-        params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID);
-        params.grantedRuntimePermissions = readGrantedRuntimePermissions(in);
-        params.installReason = readIntAttribute(in, ATTR_INSTALL_REASON);
-
-        final File appIconFile = buildAppIconFile(sessionId);
-        if (appIconFile.exists()) {
-            params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath());
-            params.appIconLastModified = appIconFile.lastModified();
-        }
-
-        return new PackageInstallerSession(mInternalCallback, mContext, mPm,
-                mInstallThread.getLooper(), sessionId, userId, installerPackageName, installerUid,
-                params, createdMillis, stageDir, stageCid, prepared, sealed);
+        mHistoricalSessionsByInstaller.put(installerUid,
+                mHistoricalSessionsByInstaller.get(installerUid) + 1);
     }
 
     private void writeSessionsLocked() {
@@ -460,7 +380,7 @@
             final int size = mSessions.size();
             for (int i = 0; i < size; i++) {
                 final PackageInstallerSession session = mSessions.valueAt(i);
-                writeSessionLocked(out, session);
+                session.write(out, mSessionsDir);
             }
             out.endTag(null, TAG_SESSIONS);
             out.endDocument();
@@ -473,106 +393,6 @@
         }
     }
 
-    private void writeSessionLocked(XmlSerializer out, PackageInstallerSession session)
-            throws IOException {
-        final SessionParams params = session.params;
-
-        out.startTag(null, TAG_SESSION);
-
-        writeIntAttribute(out, ATTR_SESSION_ID, session.sessionId);
-        writeIntAttribute(out, ATTR_USER_ID, session.userId);
-        writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME,
-                session.installerPackageName);
-        writeIntAttribute(out, ATTR_INSTALLER_UID, session.installerUid);
-        writeLongAttribute(out, ATTR_CREATED_MILLIS, session.createdMillis);
-        if (session.stageDir != null) {
-            writeStringAttribute(out, ATTR_SESSION_STAGE_DIR,
-                    session.stageDir.getAbsolutePath());
-        }
-        if (session.stageCid != null) {
-            writeStringAttribute(out, ATTR_SESSION_STAGE_CID, session.stageCid);
-        }
-        writeBooleanAttribute(out, ATTR_PREPARED, session.isPrepared());
-        writeBooleanAttribute(out, ATTR_SEALED, session.isSealed());
-
-        writeIntAttribute(out, ATTR_MODE, params.mode);
-        writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags);
-        writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation);
-        writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes);
-        writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName);
-        writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel);
-        writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri);
-        writeIntAttribute(out, ATTR_ORIGINATING_UID, params.originatingUid);
-        writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri);
-        writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride);
-        writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid);
-        writeIntAttribute(out, ATTR_INSTALL_REASON, params.installReason);
-
-        // Persist app icon if changed since last written
-        final File appIconFile = buildAppIconFile(session.sessionId);
-        if (params.appIcon == null && appIconFile.exists()) {
-            appIconFile.delete();
-        } else if (params.appIcon != null
-                && appIconFile.lastModified() != params.appIconLastModified) {
-            if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile);
-            FileOutputStream os = null;
-            try {
-                os = new FileOutputStream(appIconFile);
-                params.appIcon.compress(CompressFormat.PNG, 90, os);
-            } catch (IOException e) {
-                Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage());
-            } finally {
-                IoUtils.closeQuietly(os);
-            }
-
-            params.appIconLastModified = appIconFile.lastModified();
-        }
-
-        writeGrantedRuntimePermissions(out, params.grantedRuntimePermissions);
-
-        out.endTag(null, TAG_SESSION);
-    }
-
-    private static void writeGrantedRuntimePermissions(XmlSerializer out,
-            String[] grantedRuntimePermissions) throws IOException {
-        if (grantedRuntimePermissions != null) {
-            for (String permission : grantedRuntimePermissions) {
-                out.startTag(null, TAG_GRANTED_RUNTIME_PERMISSION);
-                writeStringAttribute(out, ATTR_NAME, permission);
-                out.endTag(null, TAG_GRANTED_RUNTIME_PERMISSION);
-            }
-        }
-    }
-
-    private static String[] readGrantedRuntimePermissions(XmlPullParser in)
-            throws IOException, XmlPullParserException {
-        List<String> permissions = null;
-
-        final int outerDepth = in.getDepth();
-        int type;
-        while ((type = in.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) {
-            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                continue;
-            }
-            if (TAG_GRANTED_RUNTIME_PERMISSION.equals(in.getName())) {
-                String permission = readStringAttribute(in, ATTR_NAME);
-                if (permissions == null) {
-                    permissions = new ArrayList<>();
-                }
-                permissions.add(permission);
-            }
-        }
-
-        if (permissions == null) {
-            return null;
-        }
-
-        String[] permissionsArray = new String[permissions.size()];
-        permissions.toArray(permissionsArray);
-        return permissionsArray;
-    }
-
     private File buildAppIconFile(int sessionId) {
         return new File(mSessionsDir, "app_icon." + sessionId + ".png");
     }
@@ -885,9 +705,11 @@
         synchronized (mSessions) {
             for (int i = 0; i < mSessions.size(); i++) {
                 final PackageInstallerSession session = mSessions.valueAt(i);
-                if (Objects.equals(session.installerPackageName, installerPackageName)
+
+                SessionInfo info = session.generateInfo(false);
+                if (Objects.equals(info.getInstallerPackageName(), installerPackageName)
                         && session.userId == userId) {
-                    result.add(session.generateInfo(false));
+                    result.add(info);
                 }
             }
         }
@@ -962,7 +784,7 @@
         final int size = sessions.size();
         for (int i = 0; i < size; i++) {
             final PackageInstallerSession session = sessions.valueAt(i);
-            if (session.installerUid == installerUid) {
+            if (session.getInstallerUid() == installerUid) {
                 count++;
             }
         }
@@ -974,7 +796,7 @@
         if (callingUid == Process.ROOT_UID) {
             return true;
         } else {
-            return (session != null) && (callingUid == session.installerUid);
+            return (session != null) && (callingUid == session.getInstallerUid());
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 5823771..2655b1c 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -25,9 +25,22 @@
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_WRONLY;
 
+import static com.android.internal.util.XmlUtils.readBitmapAttribute;
+import static com.android.internal.util.XmlUtils.readBooleanAttribute;
+import static com.android.internal.util.XmlUtils.readIntAttribute;
+import static com.android.internal.util.XmlUtils.readLongAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.readUriAttribute;
+import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
+import static com.android.internal.util.XmlUtils.writeIntAttribute;
+import static com.android.internal.util.XmlUtils.writeLongAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
+import static com.android.internal.util.XmlUtils.writeUriAttribute;
 import static com.android.server.pm.PackageInstallerService.prepareExternalStageCid;
 import static com.android.server.pm.PackageInstallerService.prepareStageDir;
 
+import android.Manifest;
+import android.annotation.NonNull;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
@@ -45,6 +58,8 @@
 import android.content.pm.PackageParser.PackageLite;
 import android.content.pm.PackageParser.PackageParserException;
 import android.content.pm.Signature;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.FileBridge;
@@ -53,6 +68,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.RevocableFileDescriptor;
@@ -80,9 +96,14 @@
 import libcore.io.IoUtils;
 import libcore.io.Libcore;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileFilter;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.security.cert.Certificate;
 import java.util.ArrayList;
@@ -97,6 +118,34 @@
 
     private static final int MSG_COMMIT = 0;
 
+    /** XML constants used for persisting a session */
+    static final String TAG_SESSION = "session";
+    private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission";
+    private static final String ATTR_SESSION_ID = "sessionId";
+    private static final String ATTR_USER_ID = "userId";
+    private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName";
+    private static final String ATTR_INSTALLER_UID = "installerUid";
+    private static final String ATTR_CREATED_MILLIS = "createdMillis";
+    private static final String ATTR_SESSION_STAGE_DIR = "sessionStageDir";
+    private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid";
+    private static final String ATTR_PREPARED = "prepared";
+    private static final String ATTR_SEALED = "sealed";
+    private static final String ATTR_MODE = "mode";
+    private static final String ATTR_INSTALL_FLAGS = "installFlags";
+    private static final String ATTR_INSTALL_LOCATION = "installLocation";
+    private static final String ATTR_SIZE_BYTES = "sizeBytes";
+    private static final String ATTR_APP_PACKAGE_NAME = "appPackageName";
+    @Deprecated
+    private static final String ATTR_APP_ICON = "appIcon";
+    private static final String ATTR_APP_LABEL = "appLabel";
+    private static final String ATTR_ORIGINATING_URI = "originatingUri";
+    private static final String ATTR_ORIGINATING_UID = "originatingUid";
+    private static final String ATTR_REFERRER_URI = "referrerUri";
+    private static final String ATTR_ABI_OVERRIDE = "abiOverride";
+    private static final String ATTR_VOLUME_UUID = "volumeUuid";
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_INSTALL_REASON = "installRason";
+
     // TODO: enforce INSTALL_ALLOW_TEST
     // TODO: enforce INSTALL_ALLOW_DOWNGRADE
 
@@ -104,12 +153,9 @@
     private final Context mContext;
     private final PackageManagerService mPm;
     private final Handler mHandler;
-    private final boolean mIsInstallerDeviceOwner;
 
     final int sessionId;
     final int userId;
-    final String installerPackageName;
-    final int installerUid;
     final SessionParams params;
     final long createdMillis;
     final int defaultContainerGid;
@@ -122,6 +168,17 @@
 
     private final Object mLock = new Object();
 
+    /** Uid of the creator of this session. */
+    private final int mOriginalInstallerUid;
+
+    /** Package of the owner of the installer session */
+    @GuardedBy("mLock")
+    private String mInstallerPackageName;
+
+    /** Uid of the owner of the installer session */
+    @GuardedBy("mLock")
+    private int mInstallerUid;
+
     @GuardedBy("mLock")
     private float mClientProgress = 0;
     @GuardedBy("mLock")
@@ -132,18 +189,25 @@
     @GuardedBy("mLock")
     private float mReportedProgress = -1;
 
+    /** State of the session. */
     @GuardedBy("mLock")
     private boolean mPrepared = false;
     @GuardedBy("mLock")
     private boolean mSealed = false;
     @GuardedBy("mLock")
-    private boolean mPermissionsAccepted = false;
+    private boolean mCommitted = false;
     @GuardedBy("mLock")
     private boolean mRelinquished = false;
     @GuardedBy("mLock")
     private boolean mDestroyed = false;
 
+    /** Permissions have been accepted by the user (see {@link #setPermissionsResult}) */
+    @GuardedBy("mLock")
+    private boolean mPermissionsManuallyAccepted = false;
+
+    @GuardedBy("mLock")
     private int mFinalStatus;
+    @GuardedBy("mLock")
     private String mFinalMessage;
 
     @GuardedBy("mLock")
@@ -155,9 +219,13 @@
     private IPackageInstallObserver2 mRemoteObserver;
 
     /** Fields derived from commit parsing */
+    @GuardedBy("mLock")
     private String mPackageName;
+    @GuardedBy("mLock")
     private int mVersionCode;
+    @GuardedBy("mLock")
     private Signature[] mSignatures;
+    @GuardedBy("mLock")
     private Certificate[][] mCertificates;
 
     /**
@@ -205,32 +273,61 @@
     private final Handler.Callback mHandlerCallback = new Handler.Callback() {
         @Override
         public boolean handleMessage(Message msg) {
-            // Cache package manager data without the lock held
-            final PackageInfo pkgInfo = mPm.getPackageInfo(
-                    params.appPackageName, PackageManager.GET_SIGNATURES
-                            | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
-            final ApplicationInfo appInfo = mPm.getApplicationInfo(
-                    params.appPackageName, 0, userId);
-
             synchronized (mLock) {
                 if (msg.obj != null) {
                     mRemoteObserver = (IPackageInstallObserver2) msg.obj;
                 }
-
                 try {
-                    commitLocked(pkgInfo, appInfo);
+                    commitLocked();
                 } catch (PackageManagerException e) {
                     final String completeMsg = ExceptionUtils.getCompleteMessage(e);
                     Slog.e(TAG, "Commit of session " + sessionId + " failed: " + completeMsg);
                     destroyInternal();
                     dispatchSessionFinished(e.error, completeMsg, null);
                 }
-
-                return true;
             }
+
+            return true;
         }
     };
 
+    /**
+     * @return {@code true} iff the installing is app an device owner?
+     */
+    private boolean isInstallerDeviceOwnerLocked() {
+        DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
+                Context.DEVICE_POLICY_SERVICE);
+
+        return (dpm != null) && dpm.isDeviceOwnerAppOnCallingUser(
+                mInstallerPackageName);
+    }
+
+    /**
+     * Checks if the permissions still need to be confirmed.
+     *
+     * <p>This is dependant on the identity of the installer, hence this cannot be cached if the
+     * installer might still {@link #transfer(String) change}.
+     *
+     * @return {@code true} iff we need to ask to confirm the permissions?
+     */
+    private boolean needToAskForPermissionsLocked() {
+        if (mPermissionsManuallyAccepted) {
+            return false;
+        }
+
+        final boolean isPermissionGranted =
+                (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES,
+                        mInstallerUid) == PackageManager.PERMISSION_GRANTED);
+        final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID);
+        final boolean forcePermissionPrompt =
+                (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0;
+
+        // Device owners are allowed to silently install packages, so the permission check is
+        // waived if the installer is the device owner.
+        return forcePermissionPrompt || !(isPermissionGranted || isInstallerRoot
+                || isInstallerDeviceOwnerLocked());
+    }
+
     public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
             Context context, PackageManagerService pm, Looper looper, int sessionId, int userId,
             String installerPackageName, int installerUid, SessionParams params, long createdMillis,
@@ -242,8 +339,9 @@
 
         this.sessionId = sessionId;
         this.userId = userId;
-        this.installerPackageName = installerPackageName;
-        this.installerUid = installerUid;
+        mOriginalInstallerUid = installerUid;
+        mInstallerPackageName = installerPackageName;
+        mInstallerUid = installerUid;
         this.params = params;
         this.createdMillis = createdMillis;
         this.stageDir = stageDir;
@@ -257,26 +355,6 @@
         mPrepared = prepared;
         mSealed = sealed;
 
-        // Device owners are allowed to silently install packages, so the permission check is
-        // waived if the installer is the device owner.
-        DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
-                Context.DEVICE_POLICY_SERVICE);
-        final boolean isPermissionGranted =
-                (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES, installerUid)
-                        == PackageManager.PERMISSION_GRANTED);
-        final boolean isInstallerRoot = (installerUid == Process.ROOT_UID);
-        final boolean forcePermissionPrompt =
-                (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0;
-        mIsInstallerDeviceOwner = (dpm != null) && dpm.isDeviceOwnerAppOnCallingUser(
-                installerPackageName);
-        if ((isPermissionGranted
-                        || isInstallerRoot
-                        || mIsInstallerDeviceOwner)
-                && !forcePermissionPrompt) {
-            mPermissionsAccepted = true;
-        } else {
-            mPermissionsAccepted = false;
-        }
         final long identity = Binder.clearCallingIdentity();
         try {
             final int uid = mPm.getPackageUid(PackageManagerService.DEFAULT_CONTAINER_PACKAGE,
@@ -295,7 +373,7 @@
         final SessionInfo info = new SessionInfo();
         synchronized (mLock) {
             info.sessionId = sessionId;
-            info.installerPackageName = installerPackageName;
+            info.installerPackageName = mInstallerPackageName;
             info.resolvedBaseCodePath = (mResolvedBaseFile != null) ?
                     mResolvedBaseFile.getAbsolutePath() : null;
             info.progress = mProgress;
@@ -326,14 +404,26 @@
         }
     }
 
-    private void assertPreparedAndNotSealed(String cookie) {
-        synchronized (mLock) {
-            if (!mPrepared) {
-                throw new IllegalStateException(cookie + " before prepared");
-            }
-            if (mSealed) {
-                throw new SecurityException(cookie + " not allowed after commit");
-            }
+    private void assertPreparedAndNotSealedLocked(String cookie) {
+        assertPreparedAndNotCommittedOrDestroyedLocked(cookie);
+        if (mSealed) {
+            throw new SecurityException(cookie + " not allowed after sealing");
+        }
+    }
+
+    private void assertPreparedAndNotCommittedOrDestroyedLocked(String cookie) {
+        assertPreparedAndNotDestroyedLocked(cookie);
+        if (mCommitted) {
+            throw new SecurityException(cookie + " not allowed after commit");
+        }
+    }
+
+    private void assertPreparedAndNotDestroyedLocked(String cookie) {
+        if (!mPrepared) {
+            throw new IllegalStateException(cookie + " before prepared");
+        }
+        if (mDestroyed) {
+            throw new SecurityException(cookie + " not allowed after destruction");
         }
     }
 
@@ -342,27 +432,27 @@
      * might point at an ASEC mount point, which is why we delay path resolution
      * until someone actively works with the session.
      */
-    private File resolveStageDir() throws IOException {
-        synchronized (mLock) {
-            if (mResolvedStageDir == null) {
-                if (stageDir != null) {
-                    mResolvedStageDir = stageDir;
+    private File resolveStageDirLocked() throws IOException {
+        if (mResolvedStageDir == null) {
+            if (stageDir != null) {
+                mResolvedStageDir = stageDir;
+            } else {
+                final String path = PackageHelper.getSdDir(stageCid);
+                if (path != null) {
+                    mResolvedStageDir = new File(path);
                 } else {
-                    final String path = PackageHelper.getSdDir(stageCid);
-                    if (path != null) {
-                        mResolvedStageDir = new File(path);
-                    } else {
-                        throw new IOException("Failed to resolve path to container " + stageCid);
-                    }
+                    throw new IOException("Failed to resolve path to container " + stageCid);
                 }
             }
-            return mResolvedStageDir;
         }
+        return mResolvedStageDir;
     }
 
     @Override
     public void setClientProgress(float progress) {
         synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+
             // Always publish first staging movement
             final boolean forcePublish = (mClientProgress == 0);
             mClientProgress = progress;
@@ -373,6 +463,8 @@
     @Override
     public void addClientProgress(float progress) {
         synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+
             setClientProgress(mClientProgress + progress);
         }
     }
@@ -390,11 +482,15 @@
 
     @Override
     public String[] getNames() {
-        assertPreparedAndNotSealed("getNames");
-        try {
-            return resolveStageDir().list();
-        } catch (IOException e) {
-            throw ExceptionUtils.wrap(e);
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotCommittedOrDestroyedLocked("getNames");
+
+            try {
+                return resolveStageDirLocked().list();
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
         }
     }
 
@@ -403,20 +499,26 @@
         if (TextUtils.isEmpty(params.appPackageName)) {
             throw new IllegalStateException("Must specify package name to remove a split");
         }
-        try {
-            createRemoveSplitMarker(splitName);
-        } catch (IOException e) {
-            throw ExceptionUtils.wrap(e);
+
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotCommittedOrDestroyedLocked("removeSplit");
+
+            try {
+                createRemoveSplitMarkerLocked(splitName);
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
         }
     }
 
-    private void createRemoveSplitMarker(String splitName) throws IOException {
+    private void createRemoveSplitMarkerLocked(String splitName) throws IOException {
         try {
             final String markerName = splitName + REMOVE_SPLIT_MARKER_EXTENSION;
             if (!FileUtils.isValidExtFilename(markerName)) {
                 throw new IllegalArgumentException("Invalid marker: " + markerName);
             }
-            final File target = new File(resolveStageDir(), markerName);
+            final File target = new File(resolveStageDirLocked(), markerName);
             target.createNewFile();
             Os.chmod(target.getAbsolutePath(), 0 /*mode*/);
         } catch (ErrnoException e) {
@@ -440,8 +542,10 @@
         // will block any attempted install transitions.
         final RevocableFileDescriptor fd;
         final FileBridge bridge;
+        final File stageDir;
         synchronized (mLock) {
-            assertPreparedAndNotSealed("openWrite");
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotSealedLocked("openWrite");
 
             if (PackageInstaller.ENABLE_REVOCABLE_FD) {
                 fd = new RevocableFileDescriptor();
@@ -452,6 +556,8 @@
                 bridge = new FileBridge();
                 mBridges.add(bridge);
             }
+
+            stageDir = resolveStageDirLocked();
         }
 
         try {
@@ -462,7 +568,7 @@
             final File target;
             final long identity = Binder.clearCallingIdentity();
             try {
-                target = new File(resolveStageDir(), name);
+                target = new File(stageDir, name);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
@@ -500,55 +606,108 @@
 
     @Override
     public ParcelFileDescriptor openRead(String name) {
-        try {
-            return openReadInternal(name);
-        } catch (IOException e) {
-            throw ExceptionUtils.wrap(e);
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotCommittedOrDestroyedLocked("openRead");
+            try {
+                return openReadInternalLocked(name);
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
         }
     }
 
-    private ParcelFileDescriptor openReadInternal(String name) throws IOException {
-        assertPreparedAndNotSealed("openRead");
-
+    private ParcelFileDescriptor openReadInternalLocked(String name) throws IOException {
         try {
             if (!FileUtils.isValidExtFilename(name)) {
                 throw new IllegalArgumentException("Invalid name: " + name);
             }
-            final File target = new File(resolveStageDir(), name);
-
+            final File target = new File(resolveStageDirLocked(), name);
             final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), O_RDONLY, 0);
             return new ParcelFileDescriptor(targetFd);
-
         } catch (ErrnoException e) {
             throw e.rethrowAsIOException();
         }
     }
 
+    /**
+     * Check if the caller is the owner of this session. Otherwise throw a
+     * {@link SecurityException}.
+     */
+    private void assertCallerIsOwnerOrRootLocked() {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != Process.ROOT_UID && callingUid != mInstallerUid) {
+            throw new SecurityException("Session does not belong to uid " + callingUid);
+        }
+    }
+
+    /**
+     * If anybody is reading or writing data of the session, throw an {@link SecurityException}.
+     */
+    private void assertNoWriteFileTransfersOpenLocked() {
+        // Verify that all writers are hands-off
+        for (RevocableFileDescriptor fd : mFds) {
+            if (!fd.isRevoked()) {
+                throw new SecurityException("Files still open");
+            }
+        }
+        for (FileBridge bridge : mBridges) {
+            if (!bridge.isClosed()) {
+                throw new SecurityException("Files still open");
+            }
+        }
+    }
+
     @Override
-    public void commit(IntentSender statusReceiver) {
+    public void commit(IntentSender statusReceiver, boolean forTransfer) {
         Preconditions.checkNotNull(statusReceiver);
 
+        // Cache package manager data without the lock held
+        final PackageInfo installedPkgInfo = mPm.getPackageInfo(
+                params.appPackageName, PackageManager.GET_SIGNATURES
+                        | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
+
         final boolean wasSealed;
         synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotDestroyedLocked("commit");
+
+            if (forTransfer) {
+                mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, null);
+
+                if (mInstallerUid == mOriginalInstallerUid) {
+                    throw new IllegalArgumentException("Session has not been transferred");
+                }
+            } else {
+                if (mInstallerUid != mOriginalInstallerUid) {
+                    throw new IllegalArgumentException("Session has been transferred");
+                }
+            }
+
             wasSealed = mSealed;
             if (!mSealed) {
-                // Verify that all writers are hands-off
-                for (RevocableFileDescriptor fd : mFds) {
-                    if (!fd.isRevoked()) {
-                        throw new SecurityException("Files still open");
-                    }
+                try {
+                    sealAndValidateLocked(installedPkgInfo);
+                } catch (PackageManagerException e) {
+                    // Do now throw an exception here to stay compatible with O and older
+                    destroyInternal();
+                    dispatchSessionFinished(e.error, ExceptionUtils.getCompleteMessage(e), null);
+                    return;
                 }
-                for (FileBridge bridge : mBridges) {
-                    if (!bridge.isClosed()) {
-                        throw new SecurityException("Files still open");
-                    }
-                }
-                mSealed = true;
             }
 
             // Client staging is fully done at this point
             mClientProgress = 1f;
             computeProgressLocked(true);
+
+            // This ongoing commit should keep session active, even though client
+            // will probably close their end.
+            mActiveCount.incrementAndGet();
+
+            mCommitted = true;
+            final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(
+                    mContext, statusReceiver, sessionId, isInstallerDeviceOwnerLocked(), userId);
+            mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget();
         }
 
         if (!wasSealed) {
@@ -557,17 +716,76 @@
             // the session lock, since otherwise it's a lock inversion.
             mCallback.onSessionSealedBlocking(this);
         }
-
-        // This ongoing commit should keep session active, even though client
-        // will probably close their end.
-        mActiveCount.incrementAndGet();
-
-        final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(mContext,
-                statusReceiver, sessionId, mIsInstallerDeviceOwner, userId);
-        mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget();
     }
 
-    private void commitLocked(PackageInfo pkgInfo, ApplicationInfo appInfo)
+    /**
+     * Seal the session to prevent further modification and validate the contents of it.
+     *
+     * <p>The session will be sealed after calling this method even if it failed.
+     *
+     * @param pkgInfo The package info for {@link #params}.packagename
+     */
+    private void sealAndValidateLocked(PackageInfo pkgInfo)
+            throws PackageManagerException {
+        assertNoWriteFileTransfersOpenLocked();
+
+        mSealed = true;
+
+        // Verify that stage looks sane with respect to existing application.
+        // This currently only ensures packageName, versionCode, and certificate
+        // consistency.
+        validateInstallLocked(pkgInfo);
+
+        // Read transfers from the original owner stay open, but as the session's data
+        // cannot be modified anymore, there is no leak of information.
+    }
+
+    @Override
+    public void transfer(String packageName) {
+        Preconditions.checkNotNull(packageName);
+
+        ApplicationInfo newOwnerAppInfo = mPm.getApplicationInfo(packageName, 0, userId);
+        if (newOwnerAppInfo == null) {
+            throw new ParcelableException(new PackageManager.NameNotFoundException(packageName));
+        }
+
+        if (PackageManager.PERMISSION_GRANTED != mPm.checkUidPermission(
+                Manifest.permission.INSTALL_PACKAGES, newOwnerAppInfo.uid)) {
+            throw new SecurityException("Destination package " + packageName + " does not have "
+                    + "the " + Manifest.permission.INSTALL_PACKAGES + " permission");
+        }
+
+        // Cache package manager data without the lock held
+        final PackageInfo installedPkgInfo = mPm.getPackageInfo(
+                params.appPackageName, PackageManager.GET_SIGNATURES
+                        | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
+
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+            assertPreparedAndNotSealedLocked("transfer");
+
+            try {
+                sealAndValidateLocked(installedPkgInfo);
+            } catch (PackageManagerException e) {
+                throw new IllegalArgumentException("Package is not valid", e);
+            }
+
+            if (!mPackageName.equals(mInstallerPackageName)) {
+                throw new SecurityException("Can only transfer sessions that update the original "
+                        + "installer");
+            }
+
+            mInstallerPackageName = packageName;
+            mInstallerUid = newOwnerAppInfo.uid;
+        }
+
+        // Persist the fact that we've sealed ourselves to prevent
+        // mutations of any hard links we create. We do this without holding
+        // the session lock, since otherwise it's a lock inversion.
+        mCallback.onSessionSealedBlocking(this);
+    }
+
+    private void commitLocked()
             throws PackageManagerException {
         if (mDestroyed) {
             throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed");
@@ -577,22 +795,17 @@
         }
 
         try {
-            resolveStageDir();
+            resolveStageDirLocked();
         } catch (IOException e) {
             throw new PackageManagerException(INSTALL_FAILED_CONTAINER_ERROR,
                     "Failed to resolve stage location", e);
         }
 
-        // Verify that stage looks sane with respect to existing application.
-        // This currently only ensures packageName, versionCode, and certificate
-        // consistency.
-        validateInstallLocked(pkgInfo, appInfo);
-
         Preconditions.checkNotNull(mPackageName);
         Preconditions.checkNotNull(mSignatures);
         Preconditions.checkNotNull(mResolvedBaseFile);
 
-        if (!mPermissionsAccepted) {
+        if (needToAskForPermissionsLocked()) {
             // User needs to accept permissions; give installer an intent they
             // can use to involve user.
             final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS);
@@ -622,7 +835,7 @@
         if (params.mode == SessionParams.MODE_INHERIT_EXISTING) {
             try {
                 final List<File> fromFiles = mResolvedInheritedFiles;
-                final File toDir = resolveStageDir();
+                final File toDir = resolveStageDirLocked();
 
                 if (LOGD) Slog.d(TAG, "Inherited files: " + mResolvedInheritedFiles);
                 if (!mResolvedInheritedFiles.isEmpty() && mInheritedFilesBase == null) {
@@ -683,7 +896,7 @@
 
         mRelinquished = true;
         mPm.installStage(mPackageName, stageDir, stageCid, localObserver, params,
-                installerPackageName, installerUid, user, mCertificates);
+                mInstallerPackageName, mInstallerUid, user, mCertificates);
     }
 
     /**
@@ -698,8 +911,7 @@
      * Note that upgrade compatibility is still performed by
      * {@link PackageManagerService}.
      */
-    private void validateInstallLocked(PackageInfo pkgInfo, ApplicationInfo appInfo)
-            throws PackageManagerException {
+    private void validateInstallLocked(PackageInfo pkgInfo) throws PackageManagerException {
         mPackageName = null;
         mVersionCode = -1;
         mSignatures = null;
@@ -752,7 +964,7 @@
                 mCertificates = apk.certificates;
             }
 
-            assertApkConsistent(String.valueOf(addedFile), apk);
+            assertApkConsistentLocked(String.valueOf(addedFile), apk);
 
             // Take this opportunity to enforce uniform naming
             final String targetName;
@@ -807,13 +1019,14 @@
 
         } else {
             // Partial installs must be consistent with existing install
-            if (appInfo == null) {
+            if (pkgInfo == null || pkgInfo.applicationInfo == null) {
                 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                         "Missing existing base package for " + mPackageName);
             }
 
             final PackageLite existing;
             final ApkLite existingBase;
+            ApplicationInfo appInfo = pkgInfo.applicationInfo;
             try {
                 existing = PackageParser.parsePackageLite(new File(appInfo.getCodePath()), 0);
                 existingBase = PackageParser.parseApkLite(new File(appInfo.getBaseCodePath()),
@@ -822,7 +1035,7 @@
                 throw PackageManagerException.from(e);
             }
 
-            assertApkConsistent("Existing base", existingBase);
+            assertApkConsistentLocked("Existing base", existingBase);
 
             // Inherit base if not overridden
             if (mResolvedBaseFile == null) {
@@ -878,7 +1091,7 @@
         }
     }
 
-    private void assertApkConsistent(String tag, ApkLite apk)
+    private void assertApkConsistentLocked(String tag, ApkLite apk)
             throws PackageManagerException {
         if (!mPackageName.equals(apk.packageName)) {
             throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package "
@@ -959,6 +1172,15 @@
         return true;
     }
 
+    /**
+     * @return the uid of the owner this session
+     */
+    public int getInstallerUid() {
+        synchronized (mLock) {
+            return mInstallerUid;
+        }
+    }
+
     private static String getRelativePath(File file, File base) throws IOException {
         final String pathStr = file.getAbsolutePath();
         final String baseStr = base.getAbsolutePath();
@@ -1106,9 +1328,9 @@
         if (accepted) {
             // Mark and kick off another install pass
             synchronized (mLock) {
-                mPermissionsAccepted = true;
+                mPermissionsManuallyAccepted = true;
+                mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
             }
-            mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
         } else {
             destroyInternal();
             dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null);
@@ -1120,7 +1342,9 @@
             mCallback.onSessionActiveChanged(this, true);
         }
 
+        boolean wasPrepared;
         synchronized (mLock) {
+            wasPrepared = mPrepared;
             if (!mPrepared) {
                 if (stageDir != null) {
                     prepareStageDir(stageDir);
@@ -1141,13 +1365,20 @@
                 }
 
                 mPrepared = true;
-                mCallback.onSessionPrepared(this);
             }
         }
+
+        if (!wasPrepared) {
+            mCallback.onSessionPrepared(this);
+        }
     }
 
     @Override
     public void close() {
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+        }
+
         if (mActiveCount.decrementAndGet() == 0) {
             mCallback.onSessionActiveChanged(this, false);
         }
@@ -1155,21 +1386,33 @@
 
     @Override
     public void abandon() {
-        if (mRelinquished) {
-            Slog.d(TAG, "Ignoring abandon after commit relinquished control");
-            return;
+        synchronized (mLock) {
+            assertCallerIsOwnerOrRootLocked();
+
+            if (mRelinquished) {
+                Slog.d(TAG, "Ignoring abandon after commit relinquished control");
+                return;
+            }
+            destroyInternal();
         }
-        destroyInternal();
+
         dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null);
     }
 
     private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
-        mFinalStatus = returnCode;
-        mFinalMessage = msg;
+        IPackageInstallObserver2 observer;
+        String packageName;
+        synchronized (mLock) {
+            mFinalStatus = returnCode;
+            mFinalMessage = msg;
 
-        if (mRemoteObserver != null) {
+            observer = mRemoteObserver;
+            packageName = mPackageName;
+        }
+
+        if (observer != null) {
             try {
-                mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras);
+                observer.onPackageInstalled(packageName, returnCode, msg, extras);
             } catch (RemoteException ignored) {
             }
         }
@@ -1220,8 +1463,9 @@
         pw.increaseIndent();
 
         pw.printPair("userId", userId);
-        pw.printPair("installerPackageName", installerPackageName);
-        pw.printPair("installerUid", installerUid);
+        pw.printPair("mOriginalInstallerUid", mOriginalInstallerUid);
+        pw.printPair("mInstallerPackageName", mInstallerPackageName);
+        pw.printPair("mInstallerUid", mInstallerUid);
         pw.printPair("createdMillis", createdMillis);
         pw.printPair("stageDir", stageDir);
         pw.printPair("stageCid", stageCid);
@@ -1232,7 +1476,7 @@
         pw.printPair("mClientProgress", mClientProgress);
         pw.printPair("mProgress", mProgress);
         pw.printPair("mSealed", mSealed);
-        pw.printPair("mPermissionsAccepted", mPermissionsAccepted);
+        pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted);
         pw.printPair("mRelinquished", mRelinquished);
         pw.printPair("mDestroyed", mDestroyed);
         pw.printPair("mFds", mFds.size());
@@ -1243,4 +1487,170 @@
 
         pw.decreaseIndent();
     }
+
+    private static void writeGrantedRuntimePermissionsLocked(XmlSerializer out,
+            String[] grantedRuntimePermissions) throws IOException {
+        if (grantedRuntimePermissions != null) {
+            for (String permission : grantedRuntimePermissions) {
+                out.startTag(null, TAG_GRANTED_RUNTIME_PERMISSION);
+                writeStringAttribute(out, ATTR_NAME, permission);
+                out.endTag(null, TAG_GRANTED_RUNTIME_PERMISSION);
+            }
+        }
+    }
+
+    private static File buildAppIconFile(int sessionId, @NonNull File sessionsDir) {
+        return new File(sessionsDir, "app_icon." + sessionId + ".png");
+    }
+
+    /**
+     * Write this session to a {@link XmlSerializer}.
+     *
+     * @param out Where to write the session to
+     * @param sessionsDir The directory containing the sessions
+     */
+    void write(@NonNull XmlSerializer out, @NonNull File sessionsDir) throws IOException {
+        synchronized (mLock) {
+            out.startTag(null, TAG_SESSION);
+
+            writeIntAttribute(out, ATTR_SESSION_ID, sessionId);
+            writeIntAttribute(out, ATTR_USER_ID, userId);
+            writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME,
+                    mInstallerPackageName);
+            writeIntAttribute(out, ATTR_INSTALLER_UID, mInstallerUid);
+            writeLongAttribute(out, ATTR_CREATED_MILLIS, createdMillis);
+            if (stageDir != null) {
+                writeStringAttribute(out, ATTR_SESSION_STAGE_DIR,
+                        stageDir.getAbsolutePath());
+            }
+            if (stageCid != null) {
+                writeStringAttribute(out, ATTR_SESSION_STAGE_CID, stageCid);
+            }
+            writeBooleanAttribute(out, ATTR_PREPARED, isPrepared());
+            writeBooleanAttribute(out, ATTR_SEALED, isSealed());
+
+            writeIntAttribute(out, ATTR_MODE, params.mode);
+            writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags);
+            writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation);
+            writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes);
+            writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName);
+            writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel);
+            writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri);
+            writeIntAttribute(out, ATTR_ORIGINATING_UID, params.originatingUid);
+            writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri);
+            writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride);
+            writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid);
+            writeIntAttribute(out, ATTR_INSTALL_REASON, params.installReason);
+
+            // Persist app icon if changed since last written
+            File appIconFile = buildAppIconFile(sessionId, sessionsDir);
+            if (params.appIcon == null && appIconFile.exists()) {
+                appIconFile.delete();
+            } else if (params.appIcon != null
+                    && appIconFile.lastModified() != params.appIconLastModified) {
+                if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile);
+                FileOutputStream os = null;
+                try {
+                    os = new FileOutputStream(appIconFile);
+                    params.appIcon.compress(Bitmap.CompressFormat.PNG, 90, os);
+                } catch (IOException e) {
+                    Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage());
+                } finally {
+                    IoUtils.closeQuietly(os);
+                }
+
+                params.appIconLastModified = appIconFile.lastModified();
+            }
+
+            writeGrantedRuntimePermissionsLocked(out, params.grantedRuntimePermissions);
+        }
+
+        out.endTag(null, TAG_SESSION);
+    }
+
+    private static String[] readGrantedRuntimePermissions(XmlPullParser in)
+            throws IOException, XmlPullParserException {
+        List<String> permissions = null;
+
+        final int outerDepth = in.getDepth();
+        int type;
+        while ((type = in.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            if (TAG_GRANTED_RUNTIME_PERMISSION.equals(in.getName())) {
+                String permission = readStringAttribute(in, ATTR_NAME);
+                if (permissions == null) {
+                    permissions = new ArrayList<>();
+                }
+                permissions.add(permission);
+            }
+        }
+
+        if (permissions == null) {
+            return null;
+        }
+
+        String[] permissionsArray = new String[permissions.size()];
+        permissions.toArray(permissionsArray);
+        return permissionsArray;
+    }
+
+    /**
+     * Read new session from a {@link XmlPullParser xml description} and create it.
+     *
+     * @param in The source of the description
+     * @param callback Callback the session uses to notify about changes of it's state
+     * @param context Context to be used by the session
+     * @param pm PackageManager to use by the session
+     * @param installerThread Thread to be used for callbacks of this session
+     * @param sessionsDir The directory the sessions are stored in
+     *
+     * @return The newly created session
+     */
+    public static PackageInstallerSession readFromXml(@NonNull XmlPullParser in,
+            @NonNull PackageInstallerService.InternalCallback callback, @NonNull Context context,
+            @NonNull PackageManagerService pm, Looper installerThread, @NonNull File sessionsDir)
+            throws IOException, XmlPullParserException {
+        final int sessionId = readIntAttribute(in, ATTR_SESSION_ID);
+        final int userId = readIntAttribute(in, ATTR_USER_ID);
+        final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME);
+        final int installerUid = readIntAttribute(in, ATTR_INSTALLER_UID, pm.getPackageUid(
+                installerPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId));
+        final long createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS);
+        final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR);
+        final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null;
+        final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID);
+        final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true);
+        final boolean sealed = readBooleanAttribute(in, ATTR_SEALED);
+
+        final SessionParams params = new SessionParams(
+                SessionParams.MODE_INVALID);
+        params.mode = readIntAttribute(in, ATTR_MODE);
+        params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS);
+        params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION);
+        params.sizeBytes = readLongAttribute(in, ATTR_SIZE_BYTES);
+        params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME);
+        params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON);
+        params.appLabel = readStringAttribute(in, ATTR_APP_LABEL);
+        params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI);
+        params.originatingUid =
+                readIntAttribute(in, ATTR_ORIGINATING_UID, SessionParams.UID_UNKNOWN);
+        params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI);
+        params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE);
+        params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID);
+        params.grantedRuntimePermissions = readGrantedRuntimePermissions(in);
+        params.installReason = readIntAttribute(in, ATTR_INSTALL_REASON);
+
+        final File appIconFile = buildAppIconFile(sessionId, sessionsDir);
+        if (appIconFile.exists()) {
+            params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath());
+            params.appIconLastModified = appIconFile.lastModified();
+        }
+
+        return new PackageInstallerSession(callback, context, pm,
+                installerThread, sessionId, userId, installerPackageName, installerUid,
+                params, createdMillis, stageDir, stageCid, prepared, sealed);
+    }
 }