summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author William Loh <wloh@google.com> 2022-11-10 05:13:46 +0000
committer William Loh <wloh@google.com> 2022-12-15 17:49:28 +0000
commite5290652c3ad867ee616b150c504c2326a7d6bd0 (patch)
tree59beabb9544f6752434e3dfd81484e1cadc6b6c4
parent64037de2b7475eadfbe0ec11dc296ba24767a0d0 (diff)
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
-rw-r--r--core/api/current.txt2
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/java/android/app/ApplicationPackageManager.java32
-rw-r--r--core/java/android/content/pm/IPackageInstallerSession.aidl3
-rw-r--r--core/java/android/content/pm/IPackageManager.aidl2
-rw-r--r--core/java/android/content/pm/PackageInstaller.java60
-rw-r--r--core/java/android/content/pm/PackageManager.java18
-rw-r--r--core/res/AndroidManifest.xml5
-rw-r--r--packages/Shell/AndroidManifest.xml3
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerSession.java116
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java17
11 files changed, 254 insertions, 5 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 39ac0d2ebad2..ae221c6c4c5e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -11809,6 +11809,7 @@ package android.content.pm {
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 @@ package android.content.pm {
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 fb742607d9d2..2cf74254d538 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3612,6 +3612,7 @@ package android.content.pm {
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 4d3f9e457ae3..309b2535798e 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -126,6 +126,11 @@ import dalvik.system.VMRuntime;
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 @@ public class ApplicationPackageManager extends PackageManager {
}
}
+ @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 7d9c64add492..60a7b13ff6e9 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -63,4 +63,7 @@ interface IPackageInstallerSession {
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 81bea2e3573f..54ca1e59aafe 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -159,6 +159,8 @@ interface IPackageManager {
*/
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 f17d8fac9ede..8deecd7f0b1b 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -59,6 +59,7 @@ import android.os.Parcel;
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 @@ public class PackageInstaller {
}
/**
+ * @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 f3ccfb0ac4f2..b6ca15932a90 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5753,6 +5753,24 @@ public abstract class PackageManager {
}
/**
+ * 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 fd4d4f8a00f4..672bc00038d0 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 47794b8427db..dc30ea01414f 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 9aaf685e9817..f65a65d6762e 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_PARSE_FAILED_NO_CERTIFIC
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.writeByteArrayAttribute;
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.icu.util.ULocale;
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.IncrementalManager;
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.FileFilter;
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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
*/
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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
}
+ 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
}
+ 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
}
- 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 759ec67cfba8..edc6b4a085f6 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 @@ public class PackageManagerService implements PackageSender, TestUtilityService
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 @@ public class PackageManagerService implements PackageSender, TestUtilityService
}
@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);