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