Merge "Delayed ASEC allocation, refine progress handling." into lmp-dev
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 0a211cf..06d4c4a 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -285,6 +285,9 @@
*
* @throws IOException if parameters were unsatisfiable, such as lack of
* disk space or unavailable media.
+ * @throws SecurityException when installation services are unavailable,
+ * such as when called from a restricted user.
+ * @throws IllegalArgumentException when {@link SessionParams} is invalid.
* @return positive, non-zero unique ID that represents the created session.
* This ID remains consistent across device reboots until the
* session is finalized. IDs are not reused during a given boot.
@@ -303,6 +306,11 @@
/**
* Open an existing session to actively perform work. To succeed, the caller
* must be the owner of the install session.
+ *
+ * @throws IOException if parameters were unsatisfiable, such as lack of
+ * disk space or unavailable media.
+ * @throws SecurityException when the caller does not own the session, or
+ * the session is invalid.
*/
public @NonNull Session openSession(int sessionId) throws IOException {
try {
@@ -319,6 +327,9 @@
* Update the icon representing the app being installed in a specific
* session. This should be roughly
* {@link ActivityManager#getLauncherLargeIconSize()} in both dimensions.
+ *
+ * @throws SecurityException when the caller does not own the session, or
+ * the session is invalid.
*/
public void updateSessionAppIcon(int sessionId, @Nullable Bitmap appIcon) {
try {
@@ -331,6 +342,9 @@
/**
* Update the label representing the app being installed in a specific
* session.
+ *
+ * @throws SecurityException when the caller does not own the session, or
+ * the session is invalid.
*/
public void updateSessionAppLabel(int sessionId, @Nullable CharSequence appLabel) {
try {
@@ -341,6 +355,15 @@
}
}
+ /**
+ * Completely abandon the given session, destroying all staged data and
+ * rendering it invalid. Abandoned sessions will be reported to
+ * {@link SessionCallback} listeners as failures. This is equivalent to
+ * opening the session and calling {@link Session#abandon()}.
+ *
+ * @throws SecurityException when the caller does not own the session, or
+ * the session is invalid.
+ */
public void abandonSession(int sessionId) {
try {
mInstaller.abandonSession(sessionId);
@@ -350,7 +373,11 @@
}
/**
- * Return details for a specific session.
+ * Return details for a specific session. No special permissions are
+ * required to retrieve these details.
+ *
+ * @return details for the requested session, or {@code null} if the session
+ * does not exist.
*/
public @Nullable SessionInfo getSessionInfo(int sessionId) {
try {
@@ -361,7 +388,7 @@
}
/**
- * Return list of all active install sessions, regardless of the installer.
+ * Return list of all known install sessions, regardless of the installer.
*/
public @NonNull List<SessionInfo> getAllSessions() {
final ApplicationInfo info = mContext.getApplicationInfo();
@@ -379,7 +406,7 @@
}
/**
- * Return list of all install sessions owned by the calling app.
+ * Return list of all known install sessions owned by the calling app.
*/
public @NonNull List<SessionInfo> getMySessions() {
try {
@@ -547,7 +574,8 @@
}
/**
- * Register to watch for session lifecycle events.
+ * Register to watch for session lifecycle events. No special permissions
+ * are required to watch for these events.
*/
public void registerSessionCallback(@NonNull SessionCallback callback) {
registerSessionCallback(callback, new Handler());
@@ -560,7 +588,8 @@
}
/**
- * Register to watch for session lifecycle events.
+ * Register to watch for session lifecycle events. No special permissions
+ * are required to watch for these events.
*
* @param handler to dispatch callback events through, otherwise uses
* calling thread.
@@ -593,7 +622,7 @@
}
/**
- * Unregister an existing callback.
+ * Unregister a previously registered callback.
*/
public void unregisterSessionCallback(@NonNull SessionCallback callback) {
synchronized (mDelegates) {
@@ -686,6 +715,12 @@
* start at the beginning of the file.
* @param lengthBytes total size of the file being written, used to
* preallocate the underlying disk space, or -1 if unknown.
+ * The system may clear various caches as needed to allocate
+ * this space.
+ * @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.
*/
public @NonNull OutputStream openWrite(@NonNull String name, long offsetBytes,
long lengthBytes) throws IOException {
@@ -719,6 +754,9 @@
* <p>
* This returns all names which have been previously written through
* {@link #openWrite(String, long, long)} as part of this session.
+ *
+ * @throws SecurityException if called after the session has been
+ * committed or abandoned.
*/
public @NonNull String[] getNames() throws IOException {
try {
@@ -738,6 +776,9 @@
* through {@link #openWrite(String, long, long)} as part of this
* session. For example, this stream may be used to calculate a
* {@link MessageDigest} of a written APK before committing.
+ *
+ * @throws SecurityException if called after the session has been
+ * committed or abandoned.
*/
public @NonNull InputStream openRead(@NonNull String name) throws IOException {
try {
@@ -759,6 +800,9 @@
* Once this method is called, no additional mutations may be performed
* on the session. If the device reboots before the session has been
* finalized, you may commit the session again.
+ *
+ * @throws SecurityException if streams opened through
+ * {@link #openWrite(String, long, long)} are still open.
*/
public void commit(@NonNull IntentSender statusReceiver) {
try {
@@ -783,7 +827,9 @@
/**
* Completely abandon this session, destroying all staged data and
- * rendering it invalid.
+ * rendering it invalid. Abandoned sessions will be reported to
+ * {@link SessionCallback} listeners as failures. This is equivalent to
+ * opening the session and calling {@link Session#abandon()}.
*/
public void abandon() {
try {
@@ -937,6 +983,18 @@
}
/** {@hide} */
+ public void setInstallFlagsInternal() {
+ installFlags |= PackageManager.INSTALL_INTERNAL;
+ installFlags &= ~PackageManager.INSTALL_EXTERNAL;
+ }
+
+ /** {@hide} */
+ public void setInstallFlagsExternal() {
+ installFlags |= PackageManager.INSTALL_EXTERNAL;
+ installFlags &= ~PackageManager.INSTALL_INTERNAL;
+ }
+
+ /** {@hide} */
public void dump(IndentingPrintWriter pw) {
pw.printPair("mode", mode);
pw.printHexPair("installFlags", installFlags);
diff --git a/core/java/android/os/FileBridge.java b/core/java/android/os/FileBridge.java
index 022a106..0acf24b 100644
--- a/core/java/android/os/FileBridge.java
+++ b/core/java/android/os/FileBridge.java
@@ -75,6 +75,13 @@
return mClosed;
}
+ public void forceClose() {
+ IoUtils.closeQuietly(mTarget);
+ IoUtils.closeQuietly(mServer);
+ IoUtils.closeQuietly(mClient);
+ mClosed = true;
+ }
+
public void setTargetFile(FileDescriptor target) {
mTarget = target;
}
@@ -89,7 +96,6 @@
try {
while (IoBridge.read(mServer, temp, 0, MSG_LENGTH) == MSG_LENGTH) {
final int cmd = Memory.peekInt(temp, 0, ByteOrder.BIG_ENDIAN);
-
if (cmd == CMD_WRITE) {
// Shuttle data into local file
int len = Memory.peekInt(temp, 4, ByteOrder.BIG_ENDIAN);
@@ -118,15 +124,10 @@
}
}
- } catch (ErrnoException e) {
- Log.wtf(TAG, "Failed during bridge", e);
- } catch (IOException e) {
+ } catch (ErrnoException | IOException e) {
Log.wtf(TAG, "Failed during bridge", e);
} finally {
- IoUtils.closeQuietly(mTarget);
- IoUtils.closeQuietly(mServer);
- IoUtils.closeQuietly(mClient);
- mClosed = true;
+ forceClose();
}
}
@@ -151,6 +152,7 @@
writeCommandAndBlock(CMD_CLOSE, "close()");
} finally {
IoBridge.closeAndSignalBlockedThreads(mClient);
+ IoUtils.closeQuietly(mClientPfd);
}
}
diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java
index c17f4ee..7bdb4be 100644
--- a/core/java/com/android/internal/content/PackageHelper.java
+++ b/core/java/com/android/internal/content/PackageHelper.java
@@ -390,7 +390,10 @@
if (!emulated && (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL)) {
final File target = new UserEnvironment(UserHandle.USER_OWNER)
.getExternalStorageDirectory();
- fitsOnExternal = (sizeBytes <= storage.getStorageBytesUntilLow(target));
+ // External is only an option when size is known
+ if (sizeBytes > 0) {
+ fitsOnExternal = (sizeBytes <= storage.getStorageBytesUntilLow(target));
+ }
}
if (prefer == RECOMMEND_INSTALL_INTERNAL) {
diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java
index 7db70ba..45d790b 100644
--- a/core/java/com/android/internal/util/XmlUtils.java
+++ b/core/java/com/android/internal/util/XmlUtils.java
@@ -1440,6 +1440,16 @@
return Boolean.parseBoolean(value);
}
+ public static boolean readBooleanAttribute(XmlPullParser in, String name,
+ boolean defaultValue) {
+ final String value = in.getAttributeValue(null, name);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ return Boolean.parseBoolean(value);
+ }
+ }
+
public static void writeBooleanAttribute(XmlSerializer out, String name, boolean value)
throws IOException {
out.attribute(null, name, Boolean.toString(value));
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 89ea905..496c136 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -42,7 +42,6 @@
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
-import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageInstallerCallback;
import android.content.pm.IPackageInstallerSession;
@@ -50,15 +49,11 @@
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
-import android.content.pm.PackageParser;
-import android.content.pm.PackageParser.PackageLite;
-import android.content.pm.PackageParser.PackageParserException;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
-import android.os.Environment.UserEnvironment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
@@ -70,7 +65,6 @@
import android.os.SELinux;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
@@ -126,6 +120,7 @@
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";
@@ -148,7 +143,6 @@
private final Context mContext;
private final PackageManagerService mPm;
private final AppOpsManager mAppOps;
- private final StorageManager mStorage;
private final File mStagingDir;
private final HandlerThread mInstallThread;
@@ -190,7 +184,6 @@
mContext = context;
mPm = pm;
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
- mStorage = StorageManager.from(mContext);
mStagingDir = stagingDir;
@@ -266,7 +259,9 @@
try {
final int sessionId = allocateSessionIdLocked();
mLegacySessions.put(sessionId, true);
- return prepareInternalStageDir(sessionId);
+ final File stageDir = buildInternalStageDir(sessionId);
+ prepareInternalStageDir(stageDir);
+ return stageDir;
} catch (IllegalStateException e) {
throw new IOException(e);
}
@@ -345,6 +340,7 @@
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(
@@ -362,7 +358,7 @@
return new PackageInstallerSession(mInternalCallback, mContext, mPm,
mInstallThread.getLooper(), sessionId, userId, installerPackageName, params,
- createdMillis, stageDir, stageCid, sealed);
+ createdMillis, stageDir, stageCid, prepared, sealed);
}
private void writeSessionsLocked() {
@@ -410,6 +406,7 @@
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);
@@ -483,14 +480,10 @@
}
}
- // TODO: treat INHERIT_EXISTING as install for user
-
- // Figure out where we're going to be staging session data
- final boolean stageInternal;
-
- if (params.mode == SessionParams.MODE_FULL_INSTALL) {
- // Brand new install, use best resolved location. This also verifies
- // that target has enough free space for the install.
+ if (params.mode == SessionParams.MODE_FULL_INSTALL
+ || params.mode == SessionParams.MODE_INHERIT_EXISTING) {
+ // Resolve best location for install, based on combination of
+ // requested install flags, delta size, and manifest settings.
final long ident = Binder.clearCallingIdentity();
try {
final int resolved = PackageHelper.resolveInstallLocation(mContext,
@@ -498,46 +491,15 @@
params.installFlags);
if (resolved == PackageHelper.RECOMMEND_INSTALL_INTERNAL) {
- stageInternal = true;
+ params.setInstallFlagsInternal();
} else if (resolved == PackageHelper.RECOMMEND_INSTALL_EXTERNAL) {
- stageInternal = false;
+ params.setInstallFlagsExternal();
} else {
throw new IOException("No storage with enough free space; res=" + resolved);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
- } else if (params.mode == SessionParams.MODE_INHERIT_EXISTING) {
- // Inheriting existing install, so stay on the same storage medium.
- final ApplicationInfo existingApp = mPm.getApplicationInfo(params.appPackageName, 0,
- userId);
- if (existingApp == null) {
- throw new IllegalStateException(
- "Missing existing app " + params.appPackageName);
- }
-
- final long existingSize;
- try {
- final PackageLite existingPkg = PackageParser.parsePackageLite(
- new File(existingApp.getCodePath()), 0);
- existingSize = PackageHelper.calculateInstalledSize(existingPkg, false,
- params.abiOverride);
- } catch (PackageParserException e) {
- throw new IllegalStateException(
- "Failed to calculate size of " + params.appPackageName);
- }
-
- if ((existingApp.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == 0) {
- // Internal we can link existing install into place, so we only
- // need enough space for the new data.
- checkInternalStorage(params.sizeBytes);
- stageInternal = true;
- } else {
- // External we're going to copy existing install into our
- // container, so we need footprint of both.
- checkExternalStorage(params.sizeBytes + existingSize);
- stageInternal = false;
- }
} else {
throw new IllegalArgumentException("Invalid install mode: " + params.mode);
}
@@ -563,15 +525,15 @@
// We're staging to exactly one location
File stageDir = null;
String stageCid = null;
- if (stageInternal) {
- stageDir = prepareInternalStageDir(sessionId);
+ if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
+ stageDir = buildInternalStageDir(sessionId);
} else {
- stageCid = prepareExternalStageCid(sessionId, params.sizeBytes);
+ stageCid = buildExternalStageCid(sessionId);
}
session = new PackageInstallerSession(mInternalCallback, mContext, mPm,
mInstallThread.getLooper(), sessionId, userId, installerPackageName, params,
- createdMillis, stageDir, stageCid, false);
+ createdMillis, stageDir, stageCid, false, false);
mSessions.put(sessionId, session);
}
@@ -615,32 +577,16 @@
}
}
- private void checkInternalStorage(long sizeBytes) throws IOException {
- if (sizeBytes <= 0) return;
-
- final File target = Environment.getDataDirectory();
- final long targetBytes = sizeBytes + mStorage.getStorageLowBytes(target);
-
- mPm.freeStorage(targetBytes);
- if (target.getUsableSpace() < targetBytes) {
- throw new IOException("Not enough internal space to write " + sizeBytes + " bytes");
- }
- }
-
- private void checkExternalStorage(long sizeBytes) throws IOException {
- if (sizeBytes <= 0) return;
-
- final File target = new UserEnvironment(UserHandle.USER_OWNER)
- .getExternalStorageDirectory();
- final long targetBytes = sizeBytes + mStorage.getStorageLowBytes(target);
-
- if (target.getUsableSpace() < targetBytes) {
- throw new IOException("Not enough external space to write " + sizeBytes + " bytes");
- }
- }
-
@Override
public IPackageInstallerSession openSession(int sessionId) {
+ try {
+ return openSessionInternal(sessionId);
+ } catch (IOException e) {
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+
+ private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
@@ -665,40 +611,37 @@
throw new IllegalStateException("Failed to allocate session ID");
}
- private File prepareInternalStageDir(int sessionId) throws IOException {
- final File file = new File(mStagingDir, "vmdl" + sessionId + ".tmp");
+ private File buildInternalStageDir(int sessionId) {
+ return new File(mStagingDir, "vmdl" + sessionId + ".tmp");
+ }
- if (file.exists()) {
- throw new IOException("Session dir already exists: " + file);
+ static void prepareInternalStageDir(File stageDir) throws IOException {
+ if (stageDir.exists()) {
+ throw new IOException("Session dir already exists: " + stageDir);
}
try {
- Os.mkdir(file.getAbsolutePath(), 0755);
- Os.chmod(file.getAbsolutePath(), 0755);
+ Os.mkdir(stageDir.getAbsolutePath(), 0755);
+ Os.chmod(stageDir.getAbsolutePath(), 0755);
} catch (ErrnoException e) {
// This purposefully throws if directory already exists
- throw new IOException("Failed to prepare session dir", e);
+ throw new IOException("Failed to prepare session dir: " + stageDir, e);
}
- if (!SELinux.restorecon(file)) {
- throw new IOException("Failed to restorecon session dir");
+ if (!SELinux.restorecon(stageDir)) {
+ throw new IOException("Failed to restorecon session dir: " + stageDir);
}
-
- return file;
}
- private String prepareExternalStageCid(int sessionId, long sizeBytes) throws IOException {
- if (sizeBytes <= 0) {
- throw new IOException("Session must provide valid size for ASEC");
- }
+ private String buildExternalStageCid(int sessionId) {
+ return "smdl" + sessionId + ".tmp";
+ }
- final String cid = "smdl" + sessionId + ".tmp";
- if (PackageHelper.createSdDir(sizeBytes, cid, PackageManagerService.getEncryptKey(),
+ static void prepareExternalStageCid(String stageCid, long sizeBytes) throws IOException {
+ if (PackageHelper.createSdDir(sizeBytes, stageCid, PackageManagerService.getEncryptKey(),
Process.SYSTEM_UID, true) == null) {
- throw new IOException("Failed to create ASEC");
+ throw new IOException("Failed to create session cid: " + stageCid);
}
-
- return cid;
}
@Override
@@ -1031,6 +974,12 @@
writeSessionsAsync();
}
+ public void onSessionPrepared(PackageInstallerSession session) {
+ // We prepared the destination to write into; we want to persist
+ // this, but it's not critical enough to block for.
+ writeSessionsAsync();
+ }
+
public void onSessionSealed(PackageInstallerSession session) {
// It's very important that we block until we've recorded the
// session as being sealed, since we never want to allow mutation
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f8273c0..adca46a 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -17,15 +17,15 @@
package com.android.server.pm;
import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
-import static android.content.pm.PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
import static android.content.pm.PackageManager.INSTALL_FAILED_CONTAINER_ERROR;
import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
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.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_WRONLY;
+import static com.android.server.pm.PackageInstallerService.prepareExternalStageCid;
+import static com.android.server.pm.PackageInstallerService.prepareInternalStageDir;
import android.content.Context;
import android.content.Intent;
@@ -88,8 +88,6 @@
// TODO: enforce INSTALL_ALLOW_TEST
// TODO: enforce INSTALL_ALLOW_DOWNGRADE
- // TODO: treat INHERIT_EXISTING as installExistingPackage()
-
private final PackageInstallerService.InternalCallback mCallback;
private final Context mContext;
private final PackageManagerService mPm;
@@ -108,18 +106,23 @@
/** Note that UID is not persisted; it's always derived at runtime. */
final int installerUid;
- private final AtomicInteger mOpenCount = new AtomicInteger();
+ private final AtomicInteger mActiveCount = new AtomicInteger();
private final Object mLock = new Object();
@GuardedBy("mLock")
private float mClientProgress = 0;
@GuardedBy("mLock")
+ private float mInternalProgress = 0;
+
+ @GuardedBy("mLock")
private float mProgress = 0;
@GuardedBy("mLock")
private float mReportedProgress = -1;
@GuardedBy("mLock")
+ private boolean mPrepared = false;
+ @GuardedBy("mLock")
private boolean mSealed = false;
@GuardedBy("mLock")
private boolean mPermissionsAccepted = false;
@@ -184,7 +187,7 @@
public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
Context context, PackageManagerService pm, Looper looper, int sessionId, int userId,
String installerPackageName, SessionParams params, long createdMillis,
- File stageDir, String stageCid, boolean sealed) {
+ File stageDir, String stageCid, boolean prepared, boolean sealed) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -203,6 +206,7 @@
"Exactly one of stageDir or stageCid stage must be set");
}
+ mPrepared = prepared;
mSealed = sealed;
// Always derived at runtime
@@ -214,8 +218,6 @@
} else {
mPermissionsAccepted = false;
}
-
- computeProgressLocked();
}
public SessionInfo generateInfo() {
@@ -227,7 +229,7 @@
mResolvedBaseFile.getAbsolutePath() : null;
info.progress = mProgress;
info.sealed = mSealed;
- info.active = mOpenCount.get() > 0;
+ info.active = mActiveCount.get() > 0;
info.mode = params.mode;
info.sizeBytes = params.sizeBytes;
@@ -238,14 +240,23 @@
return info;
}
+ public boolean isPrepared() {
+ synchronized (mLock) {
+ return mPrepared;
+ }
+ }
+
public boolean isSealed() {
synchronized (mLock) {
return mSealed;
}
}
- private void assertNotSealed(String cookie) {
+ 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");
}
@@ -278,30 +289,26 @@
@Override
public void setClientProgress(float progress) {
synchronized (mLock) {
+ // Always publish first staging movement
+ final boolean forcePublish = (mClientProgress == 0);
mClientProgress = progress;
- computeProgressLocked();
+ computeProgressLocked(forcePublish);
}
- maybePublishProgress();
}
@Override
public void addClientProgress(float progress) {
synchronized (mLock) {
- mClientProgress += progress;
- computeProgressLocked();
- }
- maybePublishProgress();
- }
-
- private void computeProgressLocked() {
- if (mProgress <= 0.8f) {
- mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f);
+ setClientProgress(mClientProgress + progress);
}
}
- private void maybePublishProgress() {
+ private void computeProgressLocked(boolean forcePublish) {
+ mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f)
+ + MathUtils.constrain(mInternalProgress * 0.2f, 0f, 0.2f);
+
// Only publish when meaningful change
- if (Math.abs(mProgress - mReportedProgress) > 0.01) {
+ if (forcePublish || Math.abs(mProgress - mReportedProgress) >= 0.01) {
mReportedProgress = mProgress;
mCallback.onSessionProgressChanged(this, mProgress);
}
@@ -309,7 +316,7 @@
@Override
public String[] getNames() {
- assertNotSealed("getNames");
+ assertPreparedAndNotSealed("getNames");
try {
return resolveStageDir().list();
} catch (IOException e) {
@@ -333,7 +340,7 @@
// will block any attempted install transitions.
final FileBridge bridge;
synchronized (mLock) {
- assertNotSealed("openWrite");
+ assertPreparedAndNotSealed("openWrite");
bridge = new FileBridge();
mBridges.add(bridge);
@@ -387,7 +394,7 @@
}
private ParcelFileDescriptor openReadInternal(String name) throws IOException {
- assertNotSealed("openRead");
+ assertPreparedAndNotSealed("openRead");
try {
if (!FileUtils.isValidExtFilename(name)) {
@@ -407,6 +414,30 @@
public void commit(IntentSender statusReceiver) {
Preconditions.checkNotNull(statusReceiver);
+ synchronized (mLock) {
+ if (!mSealed) {
+ // Verify that all writers are hands-off
+ for (FileBridge bridge : mBridges) {
+ if (!bridge.isClosed()) {
+ throw new SecurityException("Files still open");
+ }
+ }
+
+ // Persist the fact that we've sealed ourselves to prevent
+ // mutations of any hard links we create.
+ mSealed = true;
+ mCallback.onSessionSealed(this);
+ }
+ }
+
+ // 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();
+
final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(mContext,
statusReceiver, sessionId);
mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget();
@@ -414,22 +445,10 @@
private void commitLocked() throws PackageManagerException {
if (mDestroyed) {
- throw new PackageManagerException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session");
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed");
}
-
- // Verify that all writers are hands-off
if (!mSealed) {
- for (FileBridge bridge : mBridges) {
- if (!bridge.isClosed()) {
- throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
- "Files still open");
- }
- }
- mSealed = true;
-
- // Persist the fact that we've sealed ourselves to prevent mutations
- // of any hard links we create below.
- mCallback.onSessionSealed(this);
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed");
}
try {
@@ -458,6 +477,10 @@
mRemoteObserver.onUserActionRequired(intent);
} catch (RemoteException ignored) {
}
+
+ // Commit was keeping session marked as active until now; release
+ // that extra refcount so session appears idle.
+ close();
return;
}
@@ -487,8 +510,8 @@
}
// TODO: surface more granular state from dexopt
- mProgress = 0.9f;
- maybePublishProgress();
+ mInternalProgress = 0.5f;
+ computeProgressLocked(true);
// Unpack native libraries
extractNativeLibraries(mResolvedStageDir, params.abiOverride);
@@ -831,15 +854,35 @@
}
}
- public void open() {
- if (mOpenCount.getAndIncrement() == 0) {
+ public void open() throws IOException {
+ if (mActiveCount.getAndIncrement() == 0) {
mCallback.onSessionActiveChanged(this, true);
}
+
+ synchronized (mLock) {
+ if (!mPrepared) {
+ if (stageDir != null) {
+ prepareInternalStageDir(stageDir);
+ } else if (stageCid != null) {
+ prepareExternalStageCid(stageCid, params.sizeBytes);
+
+ // TODO: deliver more granular progress for ASEC allocation
+ mInternalProgress = 0.25f;
+ computeProgressLocked(true);
+ } else {
+ throw new IllegalArgumentException(
+ "Exactly one of stageDir or stageCid stage must be set");
+ }
+
+ mPrepared = true;
+ mCallback.onSessionPrepared(this);
+ }
+ }
}
@Override
public void close() {
- if (mOpenCount.decrementAndGet() == 0) {
+ if (mActiveCount.decrementAndGet() == 0) {
mCallback.onSessionActiveChanged(this, false);
}
}
@@ -869,6 +912,11 @@
synchronized (mLock) {
mSealed = true;
mDestroyed = true;
+
+ // Force shut down all bridges
+ for (FileBridge bridge : mBridges) {
+ bridge.forceClose();
+ }
}
if (stageDir != null) {
FileUtils.deleteContents(stageDir);