Add new getAppMetadata and setAppMetadata APIs

To support Android Safety Labels, PM will store App metadata that can
be optionally provided by installers.

The PackageInstaller.Session.setAppMetadata API can be used to add app
metadata as a PersistableBundle. Due to the size of this data, it will
be written directly to file on the server via a fd.

The PackageInstaller.Session.getAppMetadata API can be used to fetch
any app metadata that may have been set.

The PackageManager.getAppMetadata system API can be used to fetch this
data as a PersistableBundle for installed apps.

Bug: 252811917
Test: atest CtsPackageInstallTestCases:android.packageinstaller.install.cts.InstallAppMetadataTest
CTS-Coverage-Bug: 262054444
Change-Id: I883e40dc4bc018875be3af096a395abe7d5ba445
diff --git a/core/api/current.txt b/core/api/current.txt
index 39ac0d2e..ae221c6 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -11809,6 +11809,7 @@
     method public void close();
     method public void commit(@NonNull android.content.IntentSender);
     method public void fsync(@NonNull java.io.OutputStream) throws java.io.IOException;
+    method @NonNull public android.os.PersistableBundle getAppMetadata();
     method @NonNull public int[] getChildSessionIds();
     method @NonNull public String[] getNames() throws java.io.IOException;
     method public int getParentSessionId();
@@ -11821,6 +11822,7 @@
     method public void removeSplit(@NonNull String) throws java.io.IOException;
     method public void requestChecksums(@NonNull String, int, @NonNull java.util.List<java.security.cert.Certificate>, @NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageManager.OnChecksumsReadyListener) throws java.security.cert.CertificateEncodingException, java.io.FileNotFoundException;
     method public void requestUserPreapproval(@NonNull android.content.pm.PackageInstaller.PreapprovalDetails, @NonNull android.content.IntentSender);
+    method public void setAppMetadata(@Nullable android.os.PersistableBundle) throws java.io.IOException;
     method @Deprecated public void setChecksums(@NonNull String, @NonNull java.util.List<android.content.pm.Checksum>, @Nullable byte[]) throws java.io.IOException;
     method public void setStagingProgress(float);
     method public void transfer(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fb74260..2cf7425 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3612,6 +3612,7 @@
     method public abstract boolean arePermissionsIndividuallyControlled();
     method @NonNull public boolean[] canPackageQuery(@NonNull String, @NonNull String[]) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String);
+    method @NonNull @RequiresPermission("android.permission.GET_APP_METADATA") public android.os.PersistableBundle getAppMetadata(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.ApplicationInfoFlags, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull public android.content.pm.dex.ArtManager getArtManager();
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 4d3f9e4..309b253 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -126,6 +126,11 @@
 
 import libcore.util.EmptyArray;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.ref.WeakReference;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateEncodingException;
@@ -1223,6 +1228,33 @@
         }
     }
 
+    @Override
+    @NonNull
+    public PersistableBundle getAppMetadata(@NonNull String packageName)
+            throws NameNotFoundException {
+        PersistableBundle appMetadata = null;
+        String path = null;
+        try {
+            path = mPM.getAppMetadataPath(packageName, getUserId());
+        } catch (ParcelableException e) {
+            e.maybeRethrow(NameNotFoundException.class);
+            throw new RuntimeException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        if (path != null) {
+            File file = new File(path);
+            try (InputStream inputStream = new FileInputStream(file)) {
+                appMetadata = PersistableBundle.readFromStream(inputStream);
+            } catch (FileNotFoundException e) {
+                // ignore and return empty bundle if app metadata does not exist
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return appMetadata != null ? appMetadata : new PersistableBundle();
+    }
+
     @SuppressWarnings("unchecked")
     @Override
     public List<PackageInfo> getPackagesHoldingPermissions(String[] permissions, int flags) {
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index 7d9c64a..60a7b13 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -63,4 +63,7 @@
     void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver);
 
     boolean isKeepApplicationEnabledSetting();
+
+    ParcelFileDescriptor getAppMetadataFd();
+    ParcelFileDescriptor openWriteAppMetadata();
 }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 81bea2e..54ca1e5 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -159,6 +159,8 @@
      */
     ParceledListSlice getInstalledPackages(long flags, in int userId);
 
+    String getAppMetadataPath(String packageName, int userId);
+
     /**
      * This implements getPackagesHoldingPermissions via a "last returned row"
      * mechanism that is not exposed in the API. This is to get around the IPC
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index f17d8fa..8deecd7 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -59,6 +59,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 import android.os.ParcelableException;
+import android.os.PersistableBundle;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemProperties;
@@ -1760,6 +1761,65 @@
         }
 
         /**
+         * @return A PersistableBundle containing the app metadata set with
+         * {@link Session#setAppMetadata(PersistableBundle)}. In the case where this data does not
+         * exist, an empty PersistableBundle is returned.
+         */
+        @NonNull
+        public PersistableBundle getAppMetadata() {
+            PersistableBundle data = null;
+            try {
+                ParcelFileDescriptor pfd = mSession.getAppMetadataFd();
+                if (pfd != null) {
+                    try (InputStream inputStream =
+                            new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+                        data = PersistableBundle.readFromStream(inputStream);
+                    }
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            return data != null ? data : new PersistableBundle();
+        }
+
+        private OutputStream openWriteAppMetadata() throws IOException {
+            try {
+                if (ENABLE_REVOCABLE_FD) {
+                    return new ParcelFileDescriptor.AutoCloseOutputStream(
+                            mSession.openWriteAppMetadata());
+                } else {
+                    final ParcelFileDescriptor clientSocket = mSession.openWriteAppMetadata();
+                    return new FileBridge.FileBridgeOutputStream(clientSocket);
+                }
+            } catch (RuntimeException e) {
+                ExceptionUtils.maybeUnwrapIOException(e);
+                throw e;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Optionally set the app metadata. The size of this data cannot exceed the maximum allowed.
+         * If no data is provided, then any existing app metadata from the previous install will be
+         * removed for the package.
+         *
+         * @param data a PersistableBundle containing the app metadata. If this is set to null then
+         *     any existing app metadata will be removed.
+         * @throws IOException if writing the data fails.
+         */
+        public void setAppMetadata(@Nullable PersistableBundle data) throws IOException {
+            if (data == null) {
+                return;
+            }
+            try (OutputStream outputStream = openWriteAppMetadata()) {
+                data.writeToStream(outputStream);
+            }
+        }
+
+        /**
          * Attempt to request the approval before committing this session.
          *
          * For installers that have been granted the
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index f3ccfb0..b6ca159 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5753,6 +5753,24 @@
     }
 
     /**
+     * Returns the app metadata for a package.
+     *
+     * @param packageName
+     * @return A PersistableBundle containing the app metadata that was provided by the installer.
+     *         In the case where a package does not have any metadata, an empty PersistableBundle is
+     *         returned.
+     * @throws NameNotFoundException if no such package is available to the caller.
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    @RequiresPermission(Manifest.permission.GET_APP_METADATA)
+    public PersistableBundle getAppMetadata(@NonNull String packageName)
+            throws NameNotFoundException {
+        throw new UnsupportedOperationException("getAppMetadata not implemented in subclass");
+    }
+
+    /**
      * Return a List of all installed packages that are currently holding any of
      * the given permissions.
      *
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index fd4d4f8..672bc00 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6847,6 +6847,11 @@
     <permission android:name="android.permission.RUN_LONG_JOBS"
                 android:protectionLevel="normal|appop"/>
 
+    <!-- Allows an app access to the installer provided app metadata.
+        @hide -->
+    <permission android:name="android.permission.GET_APP_METADATA"
+                android:protectionLevel="signature" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 47794b8..dc30ea0 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -786,6 +786,9 @@
     <!-- Permission required for CTS test - ApplicationExemptionsTests -->
     <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" />
 
+    <!-- Permission required for CTS test - CtsPackageInstallTestCases-->
+    <uses-permission android:name="android.permission.GET_APP_METADATA" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 9aaf685..f65a65d 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -34,6 +34,7 @@
 import static android.content.pm.PackageManager.INSTALL_STAGED;
 import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
 import static android.os.Process.INVALID_UID;
+import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_WRONLY;
@@ -48,6 +49,7 @@
 import static com.android.internal.util.XmlUtils.writeStringAttribute;
 import static com.android.internal.util.XmlUtils.writeUriAttribute;
 import static com.android.server.pm.PackageInstallerService.prepareStageDir;
+import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME;
 
 import android.Manifest;
 import android.annotation.AnyThread;
@@ -106,6 +108,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.FileBridge;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -125,6 +128,7 @@
 import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings.Global;
 import android.stats.devicepolicy.DevicePolicyEnums;
 import android.system.ErrnoException;
@@ -178,6 +182,7 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
 import java.security.cert.Certificate;
@@ -292,6 +297,18 @@
      */
     private static final int INVALID_TARGET_SDK_VERSION = Integer.MAX_VALUE;
 
+    /**
+     * Byte size limit for app metadata.
+     *
+     * Flag type: {@code long}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_APP_METADATA_BYTE_SIZE_LIMIT =
+            "app_metadata_byte_size_limit";
+
+    /** Default byte size limit for app metadata */
+    private static final long DEFAULT_APP_METADATA_BYTE_SIZE_LIMIT = 32000;
+
     // TODO: enforce INSTALL_ALLOW_TEST
     // TODO: enforce INSTALL_ALLOW_DOWNGRADE
 
@@ -708,6 +725,7 @@
             // entries like "lost+found".
             if (file.isDirectory()) return false;
             if (file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false;
+            if (isAppMetadata(file)) return false;
             if (DexMetadataHelper.isDexMetadataFile(file)) return false;
             if (VerityUtils.isFsveritySignatureFile(file)) return false;
             if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false;
@@ -1434,6 +1452,61 @@
         }
     }
 
+    private File getTmpAppMetadataFile() {
+        return new File(Environment.getDataAppDirectory(params.volumeUuid),
+                sessionId + "-" + APP_METADATA_FILE_NAME);
+    }
+
+    private File getStagedAppMetadataFile() {
+        File file = new File(stageDir, APP_METADATA_FILE_NAME);
+        return file.exists() ? file : null;
+    }
+
+    private static boolean isAppMetadata(String name) {
+        return name.endsWith(APP_METADATA_FILE_NAME);
+    }
+
+    private static boolean isAppMetadata(File file) {
+        return isAppMetadata(file.getName());
+    }
+
+    @Override
+    public ParcelFileDescriptor getAppMetadataFd() {
+        assertCallerIsOwnerOrRoot();
+        synchronized (mLock) {
+            assertPreparedAndNotCommittedOrDestroyedLocked("openRead");
+            try {
+                return openReadInternalLocked(APP_METADATA_FILE_NAME);
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
+        }
+    }
+
+    private static long getAppMetadataSizeLimit() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                    PROPERTY_APP_METADATA_BYTE_SIZE_LIMIT, DEFAULT_APP_METADATA_BYTE_SIZE_LIMIT);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public ParcelFileDescriptor openWriteAppMetadata() {
+        assertCallerIsOwnerOrRoot();
+        synchronized (mLock) {
+            assertPreparedAndNotSealedLocked("openWriteAppMetadata");
+        }
+        try {
+            return doWriteInternal(APP_METADATA_FILE_NAME, /* offsetBytes= */ 0,
+                    /* lengthBytes= */ -1, null);
+        } catch (IOException e) {
+            throw ExceptionUtils.wrap(e);
+        }
+    }
+
     @Override
     public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) {
         assertCanWrite(false);
@@ -1705,6 +1778,21 @@
             }
         }
 
+        File appMetadataFile = getStagedAppMetadataFile();
+        if (appMetadataFile != null) {
+            long sizeLimit = getAppMetadataSizeLimit();
+            if (appMetadataFile.length() > sizeLimit) {
+                appMetadataFile.delete();
+                throw new IllegalArgumentException(
+                        "App metadata size exceeds the maximum allowed limit of " + sizeLimit);
+            }
+            if (isIncrementalInstallation()) {
+                // Incremental requires stageDir to be empty so move the app metadata file to a
+                // temporary location and move back after commit.
+                appMetadataFile.renameTo(getTmpAppMetadataFile());
+            }
+        }
+
         dispatchSessionSealed();
     }
 
@@ -2802,7 +2890,8 @@
         }
 
         final List<File> addedFiles = getAddedApksLocked();
-        if (addedFiles.isEmpty() && removeSplitList.size() == 0) {
+        if (addedFiles.isEmpty()
+                && (removeSplitList.size() == 0 || getStagedAppMetadataFile() != null)) {
             throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                     TextUtils.formatSimple("Session: %d. No packages staged in %s", sessionId,
                           stageDir.getAbsolutePath()));
@@ -2899,10 +2988,27 @@
             }
         }
 
-        if (isIncrementalInstallation() && !isIncrementalInstallationAllowed(mPackageName)) {
-            throw new PackageManagerException(
-                    PackageManager.INSTALL_FAILED_SESSION_INVALID,
-                    "Incremental installation of this package is not allowed.");
+        if (isIncrementalInstallation()) {
+            if (!isIncrementalInstallationAllowed(mPackageName)) {
+                throw new PackageManagerException(
+                        PackageManager.INSTALL_FAILED_SESSION_INVALID,
+                        "Incremental installation of this package is not allowed.");
+            }
+            // Since we moved the staged app metadata file so that incfs can be initialized, lets
+            // now move it back.
+            File appMetadataFile = getTmpAppMetadataFile();
+            if (appMetadataFile.exists()) {
+                final IncrementalFileStorages incrementalFileStorages =
+                        getIncrementalFileStorages();
+                try {
+                    incrementalFileStorages.makeFile(APP_METADATA_FILE_NAME,
+                            Files.readAllBytes(appMetadataFile.toPath()));
+                } catch (IOException e) {
+                    Slog.e(TAG, "Failed to write app metadata to incremental storage", e);
+                } finally {
+                    appMetadataFile.delete();
+                }
+            }
         }
 
         if (mInstallerUid != mOriginalInstallerUid) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 759ec67..edc6b4a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -15,6 +15,7 @@
 
 package com.android.server.pm;
 
+import static android.Manifest.permission.GET_APP_METADATA;
 import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
 import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
 import static android.app.AppOpsManager.MODE_IGNORED;
@@ -559,6 +560,8 @@
     static final String RANDOM_DIR_PREFIX = "~~";
     static final char RANDOM_CODEPATH_PREFIX = '-';
 
+    static final String APP_METADATA_FILE_NAME = "app.metadata";
+
     final Handler mHandler;
     final Handler mBackgroundHandler;
 
@@ -5058,6 +5061,20 @@
         }
 
         @Override
+        public String getAppMetadataPath(String packageName, int userId) {
+            mContext.enforceCallingOrSelfPermission(GET_APP_METADATA, "getAppMetadataPath");
+            final int callingUid = Binder.getCallingUid();
+            final Computer snapshot = snapshotComputer();
+            final PackageStateInternal ps = snapshot.getPackageStateForInstalledAndFiltered(
+                    packageName, callingUid, userId);
+            if (ps == null) {
+                throw new ParcelableException(
+                        new PackageManager.NameNotFoundException(packageName));
+            }
+            return new File(ps.getPathString(), APP_METADATA_FILE_NAME).getAbsolutePath();
+        }
+
+        @Override
         public String getPermissionControllerPackageName() {
             final int callingUid = Binder.getCallingUid();
             final int callingUserId = UserHandle.getUserId(callingUid);