From 02bd78490d8594d225ecc70a74b2058cb968a657 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Mon, 6 Oct 2014 15:14:27 -0700 Subject: [PATCH] Reduce PackageInstaller I/O pressure. When performing a restore during initial device setup, we could be installing hundreds of packages. Currently, we're writing all metadata (including heavy icons) for every session mutation! Because we're holding the mSessions lock while writing all this heavy data, we end up causing ANRs when apps call other PackageInstaller APIs. This patch mitigates by moving the heavy icon data into separate per-session PNG files, which we only persist when changed. Bug: 17881962, 17567794 Change-Id: I4dee15d4a65a8eb65c381e6bb7477728b6cc30d2 --- .../android/content/pm/PackageInstaller.java | 2 + .../com/android/internal/util/XmlUtils.java | 1 + .../server/pm/PackageInstallerService.java | 84 +++++++++++++++++-- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index db8fac28a7d1a..f249c5fc56066 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -880,6 +880,8 @@ public class PackageInstaller { /** {@hide} */ public String appLabel; /** {@hide} */ + public long appIconLastModified = -1; + /** {@hide} */ public Uri originatingUri; /** {@hide} */ public Uri referrerUri; diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index 45d790b0dd9b6..e9baaa8c2dfea 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -1503,6 +1503,7 @@ public class XmlUtils { } } + @Deprecated public static void writeBitmapAttribute(XmlSerializer out, String name, Bitmap value) throws IOException { if (value != null) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index d5858a5425274..9db3fbaaff317 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -22,7 +22,6 @@ 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.writeBitmapAttribute; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.internal.util.XmlUtils.writeLongAttribute; @@ -47,6 +46,8 @@ import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageManager; 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; @@ -69,7 +70,6 @@ import android.text.format.DateUtils; import android.util.ArraySet; import android.util.AtomicFile; import android.util.ExceptionUtils; -import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -125,6 +125,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub { 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"; @@ -149,10 +150,16 @@ public class PackageInstallerService extends IPackageInstaller.Stub { private final Callbacks mCallbacks; /** - * File storing persisted {@link #mSessions}. + * File storing persisted {@link #mSessions} metadata. */ private final AtomicFile mSessionsFile; + /** + * Directory storing persisted {@link #mSessions} metadata which is too + * heavy to store directly in {@link #mSessionsFile}. + */ + private final File mSessionsDir; + private final InternalCallback mInternalCallback = new InternalCallback(); /** @@ -195,26 +202,38 @@ public class PackageInstallerService extends IPackageInstaller.Stub { mSessionsFile = new AtomicFile( new File(Environment.getSystemSecureDirectory(), "install_sessions.xml")); + mSessionsDir = new File(Environment.getSystemSecureDirectory(), "install_sessions"); + mSessionsDir.mkdirs(); synchronized (mSessions) { readSessionsLocked(); - final ArraySet unclaimed = Sets.newArraySet(mStagingDir.listFiles(sStageFilter)); + final ArraySet unclaimedStages = Sets.newArraySet( + mStagingDir.listFiles(sStageFilter)); + final ArraySet unclaimedIcons = Sets.newArraySet( + mSessionsDir.listFiles()); - // Ignore stages claimed by active sessions + // Ignore stages and icons claimed by active sessions for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); - unclaimed.remove(session.stageDir); + unclaimedStages.remove(session.stageDir); + unclaimedIcons.remove(buildAppIconFile(session.sessionId)); } // Clean up orphaned staging directories - for (File stage : unclaimed) { + for (File stage : unclaimedStages) { Slog.w(TAG, "Deleting orphan stage " + stage); if (stage.isDirectory()) { FileUtils.deleteContents(stage); } stage.delete(); } + + // Clean up orphaned icons + for (File icon : unclaimedIcons) { + Slog.w(TAG, "Deleting orphan icon " + icon); + icon.delete(); + } } } @@ -359,6 +378,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub { params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI); params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE); + 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); @@ -418,15 +443,38 @@ public class PackageInstallerService extends IPackageInstaller.Stub { writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation); writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes); writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName); - writeBitmapAttribute(out, ATTR_APP_ICON, params.appIcon); writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel); writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri); writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri); writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride); + // 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(); + } + out.endTag(null, TAG_SESSION); } + private File buildAppIconFile(int sessionId) { + return new File(mSessionsDir, "app_icon." + sessionId + ".png"); + } + private void writeSessionsAsync() { IoThread.getHandler().post(new Runnable() { @Override @@ -548,7 +596,21 @@ public class PackageInstallerService extends IPackageInstaller.Stub { if (session == null || !isCallingUidOwner(session)) { throw new SecurityException("Caller has no access to session " + sessionId); } + + // Defensively resize giant app icons + if (appIcon != null) { + final ActivityManager am = (ActivityManager) mContext.getSystemService( + Context.ACTIVITY_SERVICE); + final int iconSize = am.getLauncherLargeIconSize(); + if ((appIcon.getWidth() > iconSize * 2) + || (appIcon.getHeight() > iconSize * 2)) { + appIcon = Bitmap.createScaledBitmap(appIcon, iconSize, iconSize, true); + } + } + session.params.appIcon = appIcon; + session.params.appIconLastModified = -1; + mInternalCallback.onSessionBadgingChanged(session); } } @@ -973,6 +1035,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub { synchronized (mSessions) { mSessions.remove(session.sessionId); mHistoricalSessions.put(session.sessionId, session); + + final File appIconFile = buildAppIconFile(session.sessionId); + if (appIconFile.exists()) { + appIconFile.delete(); + } + writeSessionsLocked(); } }