diff options
22 files changed, 3815 insertions, 108 deletions
diff --git a/api/current.txt b/api/current.txt index 1210d627c99e..5906308afced 100644 --- a/api/current.txt +++ b/api/current.txt @@ -3977,6 +3977,7 @@ package android.app { method public android.util.Size getAppTaskThumbnailSize(); method public java.util.List<android.app.ActivityManager.AppTask> getAppTasks(); method public android.content.pm.ConfigurationInfo getDeviceConfigurationInfo(); + method @Nullable public java.util.List<android.app.ApplicationExitInfo> getHistoricalProcessExitReasons(@Nullable String, @IntRange(from=0) int, @IntRange(from=0) int); method public int getLargeMemoryClass(); method public int getLauncherLargeIconDensity(); method public int getLauncherLargeIconSize(); @@ -3994,6 +3995,7 @@ package android.app { method public boolean isActivityStartAllowedOnDisplay(@NonNull android.content.Context, int, @NonNull android.content.Intent); method public boolean isBackgroundRestricted(); method @Deprecated public boolean isInLockTaskMode(); + method public static boolean isLowMemoryKillReportSupported(); method public boolean isLowRamDevice(); method @Deprecated public static boolean isRunningInTestHarness(); method public static boolean isRunningInUserTestHarness(); @@ -4507,6 +4509,35 @@ package android.app { field public String serviceDetails; } + public final class ApplicationExitInfo implements android.os.Parcelable { + method public int describeContents(); + method public int getDefiningUid(); + method @Nullable public String getDescription(); + method public int getImportance(); + method public int getPackageUid(); + method public int getPid(); + method @NonNull public String getProcessName(); + method public int getPss(); + method public int getRealUid(); + method public int getReason(); + method public int getRss(); + method public int getStatus(); + method public long getTimestamp(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.ApplicationExitInfo> CREATOR; + field public static final int REASON_ANR = 6; // 0x6 + field public static final int REASON_CRASH = 4; // 0x4 + field public static final int REASON_CRASH_NATIVE = 5; // 0x5 + field public static final int REASON_EXCESSIVE_RESOURCE_USAGE = 9; // 0x9 + field public static final int REASON_EXIT_SELF = 1; // 0x1 + field public static final int REASON_INITIALIZATION_FAILURE = 7; // 0x7 + field public static final int REASON_LOW_MEMORY = 3; // 0x3 + field public static final int REASON_OTHER = 10; // 0xa + field public static final int REASON_PERMISSION_CHANGE = 8; // 0x8 + field public static final int REASON_SIGNALED = 2; // 0x2 + field public static final int REASON_UNKNOWN = 0; // 0x0 + } + public final class AsyncNotedAppOp implements android.os.Parcelable { method public int describeContents(); method @Nullable public String getFeatureId(); diff --git a/api/system-current.txt b/api/system-current.txt index c2f6b2feafb9..2e8aee3d3bc1 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -567,6 +567,10 @@ package android.app { field @NonNull public static final android.os.Parcelable.Creator<android.app.AppOpsManager.PackageOps> CREATOR; } + public final class ApplicationExitInfo implements android.os.Parcelable { + method @NonNull public android.os.UserHandle getUserHandle(); + } + public class BroadcastOptions { method public static android.app.BroadcastOptions makeBasic(); method @RequiresPermission("android.permission.START_ACTIVITIES_FROM_BACKGROUND") public void setBackgroundActivityStartsAllowed(boolean); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index e1bd6aeb6f48..8753b986f2df 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -22,6 +22,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import android.Manifest; import android.annotation.DrawableRes; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -3480,6 +3481,50 @@ public class ActivityManager { } /** + * Return a list of {@link ApplicationExitInfo} records containing the reasons for the most + * recent app deaths. + * + * <p class="note"> Note: System stores this historical information in a ring buffer and only + * the most recent records will be returned. </p> + * + * <p class="note"> Note: In the case that this application was bound to an external service + * with flag {@link android.content.Context#BIND_EXTERNAL_SERVICE}, the process of that external + * service will be included in this package's exit info. </p> + * + * @param packageName Optional, a null value means match all packages belonging to the + * caller's UID. If this package belongs to another UID, you must hold + * {@link android.Manifest.permission#DUMP} in order to retrieve it. + * @param pid A process ID that used to belong to this package but died later; a value + * of 0 means to ignore this parameter and return all matching records. + * @param maxNum The maximum number of results to be returned; a value of 0 + * means to ignore this parameter and return all matching records + * + * @return a list of {@link ApplicationExitInfo} records matching the criteria, sorted in + * the order from most recent to least recent. + */ + @Nullable + public List<ApplicationExitInfo> getHistoricalProcessExitReasons(@Nullable String packageName, + @IntRange(from = 0) int pid, @IntRange(from = 0) int maxNum) { + try { + ParceledListSlice<ApplicationExitInfo> r = getService().getHistoricalProcessExitReasons( + packageName, pid, maxNum, mContext.getUserId()); + return r == null ? null : r.getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /* + * @return Whether or not the low memory kill will be reported in + * {@link #getHistoricalProcessExitReasons}. + * + * @see {@link ApplicationExitInfo#REASON_LOW_MEMORY} + */ + public static boolean isLowMemoryKillReportSupported() { + return SystemProperties.getBoolean("persist.sys.lmk.reportkills", false); + } + + /** * Return the importance of a given package name, based on the processes that are * currently running. The return value is one of the importance constants defined * in {@link RunningAppProcessInfo}, giving you the highest importance of all the diff --git a/core/java/android/app/ApplicationExitInfo.aidl b/core/java/android/app/ApplicationExitInfo.aidl new file mode 100644 index 000000000000..fb7d8f605c96 --- /dev/null +++ b/core/java/android/app/ApplicationExitInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +parcelable ApplicationExitInfo; diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java new file mode 100644 index 000000000000..4bf5f07e86be --- /dev/null +++ b/core/java/android/app/ApplicationExitInfo.java @@ -0,0 +1,901 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.ActivityManager.RunningAppProcessInfo.Importance; +import android.icu.text.SimpleDateFormat; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.DebugUtils; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; +import android.util.proto.WireTypeMismatchException; + +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Date; +import java.util.Objects; + +/** + * Describes the information of an application process's death. + * + * <p> + * Application process could die for many reasons, for example {@link #REASON_LOW_MEMORY} + * when it was killed by the ystem because it was running low on memory. Reason + * of the death can be retrieved via {@link #getReason}. Besides the reason, there are a few other + * auxiliary APIs like {@link #getStatus} and {@link #getImportance} to help the caller with + * additional diagnostic information. + * </p> + * + */ +public final class ApplicationExitInfo implements Parcelable { + + /** + * Application process died due to unknown reason. + */ + public static final int REASON_UNKNOWN = 0; + + /** + * Application process exit normally by itself, for example, + * via {@link java.lang.System#exit}; {@link #getStatus} will specify the exit code. + * + * <p>Applications should normally not do this, as the system has a better knowledge + * in terms of process management.</p> + */ + public static final int REASON_EXIT_SELF = 1; + + /** + * Application process died due to the result of an OS signal; for example, + * {@link android.system.OsConstants#SIGKILL}; {@link #getStatus} will specify the signal + * number. + */ + public static final int REASON_SIGNALED = 2; + + /** + * Application process was killed by the system low memory killer, meaning the system was + * under memory pressure at the time of kill. + * + * <p class="note"> + * Not all devices support reporting {@link #REASON_LOW_MEMORY}; on a device with no such + * support, when a process is killed due to memory pressure, the {@link #getReason} will return + * {@link #REASON_SIGNALED} and {@link #getStatus} will return + * the value {@link android.system.OsConstants#SIGKILL}. + * + * Application should use {@link ActivityManager#isLowMemoryKillReportSupported} to check + * if the device supports reporting {@link #REASON_LOW_MEMORY} or not. + * </p> + */ + public static final int REASON_LOW_MEMORY = 3; + + /** + * Application process died because of an unhandled exception in Java code. + */ + public static final int REASON_CRASH = 4; + + /** + * Application process died because of a native code crash. + */ + public static final int REASON_CRASH_NATIVE = 5; + + /** + * Application process was killed due to being unresponsive (ANR). + */ + public static final int REASON_ANR = 6; + + /** + * Application process was killed because of initialization failure, + * for example, it took too long to attach to the system during the start, + * or there was an error during initialization. + */ + public static final int REASON_INITIALIZATION_FAILURE = 7; + + /** + * Application process was killed due to a runtime permission change. + */ + public static final int REASON_PERMISSION_CHANGE = 8; + + /** + * Application process was killed by the system due to excessive resource usage. + */ + public static final int REASON_EXCESSIVE_RESOURCE_USAGE = 9; + + /** + * Application process was killed by the system for various other reasons, + * for example, the application package got disabled by the user; + * {@link #getDescription} will specify the cause given by the system. + */ + public static final int REASON_OTHER = 10; + + /** + * Application process kills subreason is unknown. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_UNKNOWN = 0; + + /** + * Application process was killed because user quit it on the "wait for debugger" dialog; + * this would be set when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_WAIT_FOR_DEBUGGER = 1; + + /** + * Application process was killed by the activity manager because there were too many cached + * processes; this would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_TOO_MANY_CACHED = 2; + + /** + * Application process was killed by the activity manager because there were too many empty + * processes; this would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_TOO_MANY_EMPTY = 3; + + /** + * Application process was killed by the activity manager because there were too many cached + * processes and this process had been in empty state for a long time; + * this would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_TRIM_EMPTY = 4; + + /** + * Application process was killed by the activity manager because system was on memory pressure + * and this process took large amount of cached memory; + * this would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_LARGE_CACHED = 5; + + /** + * Application process was killed by the activity manager because the system was on low memory + * pressure for a significant amount of time since last idle; + * this would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_MEMORY_PRESSURE = 6; + + /** + * Application process was killed by the activity manager due to excessive CPU usage; + * this would be set only when the reason is {@link #REASON_EXCESSIVE_RESOURCE_USAGE}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_EXCESSIVE_CPU = 7; + + /** + * @see {@link #getPid} + */ + private int mPid; + + /** + * @see {@link #getRealUid} + */ + private int mRealUid; + + /** + * @see {@link #getPackageUid} + */ + private int mPackageUid; + + /** + * @see {@link #getDefiningUid} + */ + private int mDefiningUid; + + /** + * @see {@link #getProcessName} + */ + private String mProcessName; + + /** + * @see {@link #getReason} + */ + private @Reason int mReason; + + /** + * @see {@link #getStatus} + */ + private int mStatus; + + /** + * @see {@link #getImportance} + */ + private @Importance int mImportance; + + /** + * @see {@link #getPss} + */ + private int mPss; + + /** + * @see {@link #getRss} + */ + private int mRss; + + /** + * @see {@link #getTimestamp} + */ + private long mTimestamp; + + /** + * @see {@link #getDescription} + */ + private @Nullable String mDescription; + + /** + * @see {@link #getSubReason} + */ + private @SubReason int mSubReason; + + /** + * @see {@link #getConnectionGroup} + */ + private int mConnectionGroup; + + /** + * @see {@link #getPackageName} + */ + private String mPackageName; + + /** + * @see {@link #getPackageList} + */ + private String[] mPackageList; + + /** @hide */ + @IntDef(prefix = { "REASON_" }, value = { + REASON_UNKNOWN, + REASON_EXIT_SELF, + REASON_SIGNALED, + REASON_LOW_MEMORY, + REASON_CRASH, + REASON_CRASH_NATIVE, + REASON_ANR, + REASON_INITIALIZATION_FAILURE, + REASON_PERMISSION_CHANGE, + REASON_EXCESSIVE_RESOURCE_USAGE, + REASON_OTHER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Reason {} + + /** @hide */ + @IntDef(prefix = { "SUBREASON_" }, value = { + SUBREASON_UNKNOWN, + SUBREASON_WAIT_FOR_DEBUGGER, + SUBREASON_TOO_MANY_CACHED, + SUBREASON_TOO_MANY_EMPTY, + SUBREASON_TRIM_EMPTY, + SUBREASON_LARGE_CACHED, + SUBREASON_MEMORY_PRESSURE, + SUBREASON_EXCESSIVE_CPU, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SubReason {} + + /** + * The process id of the process that died. + */ + public int getPid() { + return mPid; + } + + /** + * The kernel user identifier of the process, most of the time the system uses this + * to do access control checks. It's typically the uid of the package where the component is + * running from, except the case of isolated process, where this field identifies the kernel + * user identifier that this process is actually running with, while the {@link #getPackageUid} + * identifies the kernel user identifier that is assigned at the package installation time. + */ + public int getRealUid() { + return mRealUid; + } + + /** + * Similar to {@link #getRealUid}, it's the kernel user identifier that is assigned at the + * package installation time. + */ + public int getPackageUid() { + return mPackageUid; + } + + /** + * Return the defining kernel user identifier, maybe different from {@link #getRealUid} and + * {@link #getPackageUid}, if an external service was bound with the flag + * {@link android.content.Context#BIND_EXTERNAL_SERVICE} - in this case, this field here + * will be the kernel user identifier of the external service provider. + */ + public int getDefiningUid() { + return mDefiningUid; + } + + /** + * The actual process name it was running with. + */ + public @NonNull String getProcessName() { + return mProcessName; + } + + /** + * The reason code of the process's death. + */ + public @Reason int getReason() { + return mReason; + } + + /* + * The exit status argument of exit() if the application calls it, or the signal + * number if the application is signaled. + */ + public int getStatus() { + return mStatus; + } + + /** + * The importance of the process that it used to have before the death. + */ + public @Importance int getImportance() { + return mImportance; + } + + /* + * Last proportional set size of the memory that the process had used in kB. + * + * <p class="note">Note: This is the value from last sampling on the process, + * it's NOT the exact memory information prior to its death; and it'll be zero + * if the process died before system had a chance to take the sample. </p> + */ + public int getPss() { + return mPss; + } + + /** + * Last resident set size of the memory that the process had used in kB. + * + * <p class="note">Note: This is the value from last sampling on the process, + * it's NOT the exact memory information prior to its death; and it'll be zero + * if the process died before system had a chance to take the sample. </p> + */ + public int getRss() { + return mRss; + } + + /** + * The timestamp of the process's death, in milliseconds since the epoch. + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * The human readable description of the process's death, given by the system; could be null. + */ + public @Nullable String getDescription() { + return mDescription; + } + + /** + * Return the user id of the record on a multi-user system. + * + * @hide + */ + @SystemApi + public @NonNull UserHandle getUserHandle() { + return UserHandle.of(UserHandle.getUserId(mRealUid)); + } + + /** + * A subtype reason in conjunction with {@link #mReason}. + * + * For internal use only. + * + * @hide + */ + public @SubReason int getSubReason() { + return mSubReason; + } + + /** + * The connection group this process belongs to, if there is any. + * @see {@link android.content.Context#updateServiceGroup}. + * + * For internal use only. + * + * @hide + */ + public int getConnectionGroup() { + return mConnectionGroup; + } + + /** + * Name of first package running in this process; + * + * For system internal use only, will not retain across processes. + * + * @hide + */ + public String getPackageName() { + return mPackageName; + } + + /** + * List of packages running in this process; + * + * For system internal use only, will not retain across processes. + * + * @hide + */ + public String[] getPackageList() { + return mPackageList; + } + + /** + * @see {@link #getPid} + * + * @hide + */ + public void setPid(final int pid) { + mPid = pid; + } + + /** + * @see {@link #getRealUid} + * + * @hide + */ + public void setRealUid(final int uid) { + mRealUid = uid; + } + + /** + * @see {@link #getPackageUid} + * + * @hide + */ + public void setPackageUid(final int uid) { + mPackageUid = uid; + } + + /** + * @see {@link #getDefiningUid} + * + * @hide + */ + public void setDefiningUid(final int uid) { + mDefiningUid = uid; + } + + /** + * @see {@link #getProcessName} + * + * @hide + */ + public void setProcessName(final String processName) { + mProcessName = processName; + } + + /** + * @see {@link #getReason} + * + * @hide + */ + public void setReason(final @Reason int reason) { + mReason = reason; + } + + /** + * @see {@link #getStatus} + * + * @hide + */ + public void setStatus(final int status) { + mStatus = status; + } + + /** + * @see {@link #getImportance} + * + * @hide + */ + public void setImportance(final @Importance int importance) { + mImportance = importance; + } + + /** + * @see {@link #getPss} + * + * @hide + */ + public void setPss(final int pss) { + mPss = pss; + } + + /** + * @see {@link #getRss} + * + * @hide + */ + public void setRss(final int rss) { + mRss = rss; + } + + /** + * @see {@link #getTimestamp} + * + * @hide + */ + public void setTimestamp(final long timestamp) { + mTimestamp = timestamp; + } + + /** + * @see {@link #getDescription} + * + * @hide + */ + public void setDescription(final String description) { + mDescription = description; + } + + /** + * @see {@link #getSubReason} + * + * @hide + */ + public void setSubReason(final @SubReason int subReason) { + mSubReason = subReason; + } + + /** + * @see {@link #getConnectionGroup} + * + * @hide + */ + public void setConnectionGroup(final int connectionGroup) { + mConnectionGroup = connectionGroup; + } + + /** + * @see {@link #getPackageName} + * + * @hide + */ + public void setPackageName(final String packageName) { + mPackageName = packageName; + } + + /** + * @see {@link #getPackageList} + * + * @hide + */ + public void setPackageList(final String[] packageList) { + mPackageList = packageList; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mPid); + dest.writeInt(mRealUid); + dest.writeInt(mPackageUid); + dest.writeInt(mDefiningUid); + dest.writeString(mProcessName); + dest.writeInt(mConnectionGroup); + dest.writeInt(mReason); + dest.writeInt(mSubReason); + dest.writeInt(mStatus); + dest.writeInt(mImportance); + dest.writeInt(mPss); + dest.writeInt(mRss); + dest.writeLong(mTimestamp); + dest.writeString(mDescription); + } + + /** @hide */ + public ApplicationExitInfo() { + } + + /** @hide */ + public ApplicationExitInfo(ApplicationExitInfo other) { + mPid = other.mPid; + mRealUid = other.mRealUid; + mPackageUid = other.mPackageUid; + mDefiningUid = other.mDefiningUid; + mProcessName = other.mProcessName; + mConnectionGroup = other.mConnectionGroup; + mReason = other.mReason; + mStatus = other.mStatus; + mSubReason = other.mSubReason; + mImportance = other.mImportance; + mPss = other.mPss; + mRss = other.mRss; + mTimestamp = other.mTimestamp; + mDescription = other.mDescription; + } + + private ApplicationExitInfo(@NonNull Parcel in) { + mPid = in.readInt(); + mRealUid = in.readInt(); + mPackageUid = in.readInt(); + mDefiningUid = in.readInt(); + mProcessName = in.readString(); + mConnectionGroup = in.readInt(); + mReason = in.readInt(); + mSubReason = in.readInt(); + mStatus = in.readInt(); + mImportance = in.readInt(); + mPss = in.readInt(); + mRss = in.readInt(); + mTimestamp = in.readLong(); + mDescription = in.readString(); + } + + public @NonNull static final Creator<ApplicationExitInfo> CREATOR = + new Creator<ApplicationExitInfo>() { + @Override + public ApplicationExitInfo createFromParcel(Parcel in) { + return new ApplicationExitInfo(in); + } + + @Override + public ApplicationExitInfo[] newArray(int size) { + return new ApplicationExitInfo[size]; + } + }; + + /** @hide */ + public void dump(@NonNull PrintWriter pw, @Nullable String prefix, @Nullable String seqSuffix, + @NonNull SimpleDateFormat sdf) { + pw.println(prefix + "ApplicationExitInfo " + seqSuffix + ":"); + pw.println(prefix + " timestamp=" + sdf.format(new Date(mTimestamp))); + pw.println(prefix + " pid=" + mPid); + pw.println(prefix + " realUid=" + mRealUid); + pw.println(prefix + " packageUid=" + mPackageUid); + pw.println(prefix + " definingUid=" + mDefiningUid); + pw.println(prefix + " user=" + UserHandle.getUserId(mPackageUid)); + pw.println(prefix + " process=" + mProcessName); + pw.println(prefix + " reason=" + mReason + " (" + reasonCodeToString(mReason) + ")"); + pw.println(prefix + " status=" + mStatus); + pw.println(prefix + " importance=" + mImportance); + pw.print(prefix + " pss="); DebugUtils.printSizeValue(pw, mPss << 10); pw.println(); + pw.print(prefix + " rss="); DebugUtils.printSizeValue(pw, mRss << 10); pw.println(); + pw.println(prefix + " description=" + mDescription); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ApplicationExitInfo(timestamp="); + sb.append(new SimpleDateFormat().format(new Date(mTimestamp))); + sb.append(" pid=").append(mPid); + sb.append(" realUid=").append(mRealUid); + sb.append(" packageUid=").append(mPackageUid); + sb.append(" definingUid=").append(mDefiningUid); + sb.append(" user=").append(UserHandle.getUserId(mPackageUid)); + sb.append(" process=").append(mProcessName); + sb.append(" reason=").append(mReason).append(" (") + .append(reasonCodeToString(mReason)).append(")"); + sb.append(" status=").append(mStatus); + sb.append(" importance=").append(mImportance); + sb.append(" pss="); DebugUtils.sizeValueToString(mPss << 10, sb); + sb.append(" rss="); DebugUtils.sizeValueToString(mRss << 10, sb); + sb.append(" description=").append(mDescription); + return sb.toString(); + } + + private static String reasonCodeToString(@Reason int reason) { + switch (reason) { + case REASON_EXIT_SELF: + return "EXIT_SELF"; + case REASON_SIGNALED: + return "SIGNALED"; + case REASON_LOW_MEMORY: + return "LOW_MEMORY"; + case REASON_CRASH: + return "APP CRASH(EXCEPTION)"; + case REASON_CRASH_NATIVE: + return "APP CRASH(NATIVE)"; + case REASON_ANR: + return "ANR"; + case REASON_INITIALIZATION_FAILURE: + return "INITIALIZATION FAILURE"; + case REASON_PERMISSION_CHANGE: + return "PERMISSION CHANGE"; + case REASON_EXCESSIVE_RESOURCE_USAGE: + return "EXCESSIVE RESOURCE USAGE"; + case REASON_OTHER: + return "OTHER KILLS BY SYSTEM"; + default: + return "UNKNOWN"; + } + } + + /** @hide */ + public static String subreasonToString(@SubReason int subreason) { + switch (subreason) { + case SUBREASON_WAIT_FOR_DEBUGGER: + return "WAIT FOR DEBUGGER"; + case SUBREASON_TOO_MANY_CACHED: + return "TOO MANY CACHED PROCS"; + case SUBREASON_TOO_MANY_EMPTY: + return "TOO MANY EMPTY PROCS"; + case SUBREASON_TRIM_EMPTY: + return "TRIM EMPTY"; + case SUBREASON_LARGE_CACHED: + return "LARGE CACHED"; + case SUBREASON_MEMORY_PRESSURE: + return "MEMORY PRESSURE"; + case SUBREASON_EXCESSIVE_CPU: + return "EXCESSIVE CPU USAGE"; + default: + return "UNKNOWN"; + } + } + + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.app.ApplicationExitInfoProto} + * + * @param proto Stream to write the ApplicationExitInfo object to. + * @param fieldId Field Id of the ApplicationExitInfo as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(ApplicationExitInfoProto.PID, mPid); + proto.write(ApplicationExitInfoProto.REAL_UID, mRealUid); + proto.write(ApplicationExitInfoProto.PACKAGE_UID, mPackageUid); + proto.write(ApplicationExitInfoProto.DEFINING_UID, mDefiningUid); + proto.write(ApplicationExitInfoProto.PROCESS_NAME, mProcessName); + proto.write(ApplicationExitInfoProto.CONNECTION_GROUP, mConnectionGroup); + proto.write(ApplicationExitInfoProto.REASON, mReason); + proto.write(ApplicationExitInfoProto.SUB_REASON, mSubReason); + proto.write(ApplicationExitInfoProto.STATUS, mStatus); + proto.write(ApplicationExitInfoProto.IMPORTANCE, mImportance); + proto.write(ApplicationExitInfoProto.PSS, mPss); + proto.write(ApplicationExitInfoProto.RSS, mRss); + proto.write(ApplicationExitInfoProto.TIMESTAMP, mTimestamp); + proto.write(ApplicationExitInfoProto.DESCRIPTION, mDescription); + proto.end(token); + } + + /** + * Read from a protocol buffer input stream. + * Protocol buffer message definition at {@link android.app.ApplicationExitInfoProto} + * + * @param proto Stream to read the ApplicationExitInfo object from. + * @param fieldId Field Id of the ApplicationExitInfo as defined in the parent message + * @hide + */ + public void readFromProto(ProtoInputStream proto, long fieldId) + throws IOException, WireTypeMismatchException { + final long token = proto.start(fieldId); + while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (proto.getFieldNumber()) { + case (int) ApplicationExitInfoProto.PID: + mPid = proto.readInt(ApplicationExitInfoProto.PID); + break; + case (int) ApplicationExitInfoProto.REAL_UID: + mRealUid = proto.readInt(ApplicationExitInfoProto.REAL_UID); + break; + case (int) ApplicationExitInfoProto.PACKAGE_UID: + mPackageUid = proto.readInt(ApplicationExitInfoProto.PACKAGE_UID); + break; + case (int) ApplicationExitInfoProto.DEFINING_UID: + mDefiningUid = proto.readInt(ApplicationExitInfoProto.DEFINING_UID); + break; + case (int) ApplicationExitInfoProto.PROCESS_NAME: + mProcessName = proto.readString(ApplicationExitInfoProto.PROCESS_NAME); + break; + case (int) ApplicationExitInfoProto.CONNECTION_GROUP: + mConnectionGroup = proto.readInt(ApplicationExitInfoProto.CONNECTION_GROUP); + break; + case (int) ApplicationExitInfoProto.REASON: + mReason = proto.readInt(ApplicationExitInfoProto.REASON); + break; + case (int) ApplicationExitInfoProto.SUB_REASON: + mSubReason = proto.readInt(ApplicationExitInfoProto.SUB_REASON); + break; + case (int) ApplicationExitInfoProto.STATUS: + mStatus = proto.readInt(ApplicationExitInfoProto.STATUS); + break; + case (int) ApplicationExitInfoProto.IMPORTANCE: + mImportance = proto.readInt(ApplicationExitInfoProto.IMPORTANCE); + break; + case (int) ApplicationExitInfoProto.PSS: + mPss = proto.readInt(ApplicationExitInfoProto.PSS); + break; + case (int) ApplicationExitInfoProto.RSS: + mRss = proto.readInt(ApplicationExitInfoProto.RSS); + break; + case (int) ApplicationExitInfoProto.TIMESTAMP: + mTimestamp = proto.readLong(ApplicationExitInfoProto.TIMESTAMP); + break; + case (int) ApplicationExitInfoProto.DESCRIPTION: + mDescription = proto.readString(ApplicationExitInfoProto.DESCRIPTION); + break; + } + } + proto.end(token); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof ApplicationExitInfo)) { + return false; + } + ApplicationExitInfo o = (ApplicationExitInfo) other; + return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid + && mDefiningUid == o.mDefiningUid + && mConnectionGroup == o.mConnectionGroup && mReason == o.mReason + && mSubReason == o.mSubReason && mImportance == o.mImportance + && mStatus == o.mStatus && mTimestamp == o.mTimestamp + && mPss == o.mPss && mRss == o.mRss + && TextUtils.equals(mProcessName, o.mProcessName) + && TextUtils.equals(mDescription, o.mDescription); + } + + @Override + public int hashCode() { + int result = mPid; + result = 31 * result + mRealUid; + result = 31 * result + mPackageUid; + result = 31 * result + mDefiningUid; + result = 31 * result + mConnectionGroup; + result = 31 * result + mReason; + result = 31 * result + mSubReason; + result = 31 * result + mImportance; + result = 31 * result + mStatus; + result = 31 * result + mPss; + result = 31 * result + mRss; + result = 31 * result + Long.hashCode(mTimestamp); + result = 31 * result + Objects.hashCode(mProcessName); + result = 31 * result + Objects.hashCode(mDescription); + return result; + } +} diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 580382e0a4aa..126cc5a6f5c4 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -18,6 +18,7 @@ package android.app; import android.app.ActivityManager; import android.app.ApplicationErrorReport; +import android.app.ApplicationExitInfo; import android.app.ContentProviderHolder; import android.app.GrantedUriPermission; import android.app.IApplicationThread; @@ -604,4 +605,30 @@ interface IActivityManager { * Method for the app to tell system that it's wedged and would like to trigger an ANR. */ void appNotResponding(String reason); + + /** + * Return a list of {@link ApplicationExitInfo} records. + * + * <p class="note"> Note: System stores these historical information in a ring buffer, older + * records would be overwritten by newer records. </p> + * + * <p class="note"> Note: In the case that this application bound to an external service with + * flag {@link android.content.Context#BIND_EXTERNAL_SERVICE}, the process of that external + * service will be included in this package's exit info. </p> + * + * @param packageName Optional, an empty value means match all packages belonging to the + * caller's UID. If this package belongs to another UID, you must hold + * {@link android.Manifest.permission#DUMP} in order to retrieve it. + * @param pid Optional, it could be a process ID that used to belong to this package but + * died later; A value of 0 means to ignore this parameter and return all + * matching records. + * @param maxNum Optional, the maximum number of results should be returned; A value of 0 + * means to ignore this parameter and return all matching records + * @param userId The userId in the multi-user environment. + * + * @return a list of {@link ApplicationExitInfo} records with the matching criteria, sorted in + * the order from most recent to least recent. + */ + ParceledListSlice<ApplicationExitInfo> getHistoricalProcessExitReasons(String packageName, + int pid, int maxNum, int userId); } diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto new file mode 100644 index 000000000000..e23f150fab25 --- /dev/null +++ b/core/proto/android/app/appexitinfo.proto @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto2"; +option java_multiple_files = true; + +package android.app; + +import "frameworks/base/core/proto/android/privacy.proto"; + +/** + * An android.app.ApplicationExitInfo object. + */ +message ApplicationExitInfoProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional int32 pid = 1; + optional int32 real_uid = 2; + optional int32 package_uid = 3; + optional int32 defining_uid = 4; + optional string process_name = 5; + optional int32 connection_group = 6; + + enum ReasonCode { + /** + * Application process died due to unknown reason. + */ + REASON_UNKNOWN = 0; + + /** + * Application process exit normally by itself, for example, + * via {@link android.os.Process#exit}; {@link #status} will specify the exit code. + * + * <p>Applications should normally not do this, as the system has a better knowledge + * in terms of process management.</p> + */ + REASON_EXIT_SELF = 1; + + /** + * Application process died due to the result of an OS signal; for example, + * {@link android.os.Process#SIGNAL_KILL}; {@link #status} will specify the signum. + */ + REASON_SIGNALED = 2; + + /** + * Application process was killed by the system low memory killer, meaning the system was + * under memory pressure at the time of kill. + */ + REASON_LOW_MEMORY = 3; + + /** + * Application process died because of an unhandled exception in Java code. + */ + REASON_CRASH = 4; + + /** + * Application process died because it's crashed due to a native code crash. + */ + REASON_CRASH_NATIVE = 5; + + /** + * Application process was killed due to being unresponsive (ANR). + */ + REASON_ANR = 6; + + /** + * Application process was killed because it took too long to attach to the system + * during the start. + */ + REASON_INITIALIZATION_FAILURE = 7; + + /** + * Application process was killed because of initialization failure, + * for example, it took too long to attach to the system during the start, + * or there was an error during initialization. + */ + REASON_PERMISSION_CHANGE = 8; + + /** + * Application process was killed by the activity manager due to excessive resource usage. + */ + REASON_EXCESSIVE_RESOURCE_USAGE = 9; + + /** + * Application process was killed by the system for various other reasons, + * for example, the application package got disabled by the user; + * {@link #description} will specify the cause given by the system. + */ + REASON_OTHER = 10; + + } + optional ReasonCode reason = 7; + + enum SubReason { + /** + * Application process kills subReason is unknown. + */ + SUBREASON_UNKNOWN = 0; + + /** + * Application process was killed because user quit it on the "wait for debugger" dialog. + */ + SUBREASON_WAIT_FOR_DEBUGGER = 1; + + /** + * Application process was killed by the activity manager because there were too many cached + * processes. + */ + SUBREASON_TOO_MANY_CACHED = 2; + + /** + * Application process was killed by the activity manager because there were too many empty + * processes. + */ + SUBREASON_TOO_MANY_EMPTY = 3; + + /** + * Application process was killed by the activity manager because there were too many cached + * processes and this process had been in empty state for a long time. + */ + SUBREASON_TRIM_EMPTY = 4; + + /** + * Application process was killed by the activity manager because system was on + * memory pressure and this process took large amount of cached memory. + */ + SUBREASON_LARGE_CACHED = 5; + + /** + * Application process was killed by the activity manager because the system was on + * low memory pressure for a significant amount of time since last idle. + */ + SUBREASON_MEMORY_PRESSURE = 6; + + /** + * Application process was killed by the activity manager due to excessive CPU usage. + */ + SUBREASON_EXCESSIVE_CPU = 7; + } + + optional SubReason sub_reason = 8; + + optional int32 status = 9; + + enum Importance { + option allow_alias = true; + /** + * Keep sync with the definitions in + * {@link android.app.ActivityManager.RunningAppProcessInfo} + */ + IMPORTANCE_FOREGROUND = 100; + IMPORTANCE_FOREGROUND_SERVICE = 125; + IMPORTANCE_TOP_SLEEPING_PRE_28 = 150; + IMPORTANCE_VISIBLE = 200; + IMPORTANCE_PERCEPTIBLE_PRE_26 = 130; + IMPORTANCE_PERCEPTIBLE = 230; + IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170; + IMPORTANCE_SERVICE = 300; + IMPORTANCE_TOP_SLEEPING = 325; + IMPORTANCE_CANT_SAVE_STATE = 350; + IMPORTANCE_CACHED = 400; + IMPORTANCE_BACKGROUND = 400; + IMPORTANCE_EMPTY = 500; + IMPORTANCE_GONE = 1000; + } + + optional Importance importance = 10; + optional int32 pss = 11; + optional int32 rss = 12; + optional int64 timestamp = 13; + optional string description = 14; +} diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index c79f314ffaf2..126d44529edc 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -19,6 +19,7 @@ syntax = "proto2"; package com.android.server.am; import "frameworks/base/core/proto/android/app/activitymanager.proto"; +import "frameworks/base/core/proto/android/app/appexitinfo.proto"; import "frameworks/base/core/proto/android/app/enums.proto"; import "frameworks/base/core/proto/android/app/notification.proto"; import "frameworks/base/core/proto/android/app/profilerinfo.proto"; @@ -1082,3 +1083,23 @@ message AppTimeTrackerProto { optional .android.util.Duration started_time = 4; optional string started_package = 5; } + +// sync with com.android.server.am.am.ProcessList.java +message AppsExitInfoProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional int64 last_update_timestamp = 1; + message Package { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional string package_name = 1; + message User { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional int32 uid = 1; + repeated .android.app.ApplicationExitInfoProto app_exit_info = 2; + } + repeated User users = 2; + } + repeated Package packages = 2; +} diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2585197d85ec..625eb63dab8f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4303,4 +4303,7 @@ <!-- Package name of the required service extension package. --> <string name="config_servicesExtensionPackage" translatable="false">android.ext.services</string> + + <!-- Retention policy: number of records to kept for the historical exit info per package. --> + <integer name="config_app_exit_info_history_list_size">16</integer> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 379d0aa3da3d..fe7ebe145717 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3820,4 +3820,7 @@ <java-symbol type="string" name="capability_title_canTakeScreenshot" /> <java-symbol type="string" name="config_servicesExtensionPackage" /> + + <!-- For app process exit info tracking --> + <java-symbol type="integer" name="config_app_exit_info_history_list_size" /> </resources> diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index e2a036aec172..3ffa5dea4d89 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -36,6 +36,7 @@ import android.app.ActivityManagerInternal; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; +import android.app.ApplicationExitInfo; import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.Notification; @@ -2730,7 +2731,7 @@ public final class ActiveServices { created = true; } catch (DeadObjectException e) { Slog.w(TAG, "Application dead when creating service " + r); - mAm.appDiedLocked(app); + mAm.appDiedLocked(app, "Died when creating service"); throw e; } finally { if (!created) { @@ -3649,7 +3650,8 @@ public final class ActiveServices { && proc.pid != 0 && proc.pid != ActivityManagerService.MY_PID && proc.setProcState >= ActivityManager.PROCESS_STATE_LAST_ACTIVITY) { proc.kill("bound to service " + sr.shortInstanceName - + " in dying proc " + (app != null ? app.processName : "??"), true); + + " in dying proc " + (app != null ? app.processName : "??"), + ApplicationExitInfo.REASON_OTHER, true); } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 883e7c6799dc..b8090dbcf809 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -155,6 +155,7 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AppOpsManagerInternal.CheckOpsDelegate; import android.app.ApplicationErrorReport; +import android.app.ApplicationExitInfo; import android.app.ApplicationThreadConstants; import android.app.BroadcastOptions; import android.app.ContentProviderHolder; @@ -684,7 +685,7 @@ public class ActivityManagerService extends IActivityManager.Stub /** * Process management. */ - final ProcessList mProcessList = new ProcessList(); + final ProcessList mProcessList; /** * Tracking long-term execution of processes to look for abuse and other @@ -1534,7 +1535,7 @@ public class ActivityManagerService extends IActivityManager.Stub TAG, "Death received in " + this + " for thread " + mAppThread.asBinder()); synchronized(ActivityManagerService.this) { - appDiedLocked(mApp, mPid, mAppThread, true); + appDiedLocked(mApp, mPid, mAppThread, true, null); } } } @@ -2382,6 +2383,7 @@ public class ActivityManagerService extends IActivityManager.Stub ? new ActivityManagerConstants(mContext, this, mHandler) : null; final ActiveUids activeUids = new ActiveUids(this, false /* postChangesToAtm */); mPlatformCompat = null; + mProcessList = injector.getProcessList(this); mProcessList.init(this, activeUids, mPlatformCompat); mLowMemDetector = null; mOomAdjuster = hasHandlerThread @@ -2436,6 +2438,7 @@ public class ActivityManagerService extends IActivityManager.Stub final ActiveUids activeUids = new ActiveUids(this, true /* postChangesToAtm */); mPlatformCompat = (PlatformCompat) ServiceManager.getService( Context.PLATFORM_COMPAT_SERVICE); + mProcessList = mInjector.getProcessList(this); mProcessList.init(this, activeUids, mPlatformCompat); mLowMemDetector = new LowMemDetector(this); mOomAdjuster = new OomAdjuster(this, mProcessList, activeUids); @@ -3680,13 +3683,13 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") - final void appDiedLocked(ProcessRecord app) { - appDiedLocked(app, app.pid, app.thread, false); + final void appDiedLocked(ProcessRecord app, String reason) { + appDiedLocked(app, app.pid, app.thread, false, reason); } @GuardedBy("this") final void appDiedLocked(ProcessRecord app, int pid, IApplicationThread thread, - boolean fromBinderDied) { + boolean fromBinderDied, String reason) { // First check if this ProcessRecord is actually active for the pid. synchronized (mPidsSelfLocked) { ProcessRecord curProc = mPidsSelfLocked.get(pid); @@ -3704,6 +3707,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (!app.killed) { if (!fromBinderDied) { killProcessQuiet(pid); + mProcessList.noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, reason); } ProcessList.killProcessGroup(app.uid, pid); app.killed = true; @@ -4727,7 +4732,7 @@ public class ActivityManagerService extends IActivityManager.Stub cleanupAppInLaunchingProvidersLocked(app, true); // Take care of any services that are waiting for the process. mServices.processStartTimedOutLocked(app); - app.kill("start timeout", true); + app.kill("start timeout", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); if (app.isolated) { mBatteryStatsService.removeIsolatedUid(app.uid, app.info.uid); } @@ -4814,6 +4819,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (pid > 0 && pid != MY_PID) { killProcessQuiet(pid); //TODO: killProcessGroup(app.info.uid, pid); + mProcessList.noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, "attach failed"); } else { try { thread.scheduleExit(); @@ -5153,7 +5160,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (badApp) { - app.kill("error during init", true); + app.kill("error during init", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); handleAppDiedLocked(app, false, true); return false; } @@ -7544,7 +7551,7 @@ public class ActivityManagerService extends IActivityManager.Stub proc.info.uid); final long ident = Binder.clearCallingIdentity(); try { - appDiedLocked(proc); + appDiedLocked(proc, "unstable content provider"); } finally { Binder.restoreCallingIdentity(ident); } @@ -8856,7 +8863,7 @@ public class ActivityManagerService extends IActivityManager.Stub } int adj = proc.setAdj; if (adj >= worstType && !proc.killedByAm) { - proc.kill(reason, true); + proc.kill(reason, ApplicationExitInfo.REASON_OTHER, true); killed = true; } } @@ -8904,7 +8911,7 @@ public class ActivityManagerService extends IActivityManager.Stub final int adj = proc.setAdj; if (adj > belowAdj && !proc.killedByAm) { - proc.kill(reason, true); + proc.kill(reason, ApplicationExitInfo.REASON_PERMISSION_CHANGE, true); killed = true; } } @@ -9048,7 +9055,10 @@ public class ActivityManagerService extends IActivityManager.Stub TimeUtils.formatDuration(lowRamSinceLastIdle, sb); Slog.wtfQuiet(TAG, sb.toString()); proc.kill("idle maint (pss " + proc.lastPss - + " from " + proc.initialIdlePss + ")", true); + + " from " + proc.initialIdlePss + ")", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_MEMORY_PRESSURE, + true); } } } else if (proc.setProcState < ActivityManager.PROCESS_STATE_HOME @@ -9145,6 +9155,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Make sure we have the current profile info, since it is needed for security checks. mUserController.onSystemReady(); mAppOpsService.systemReady(); + mProcessList.onSystemReady(); mSystemReady = true; t.traceEnd(); } @@ -9972,6 +9983,67 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public ParceledListSlice<ApplicationExitInfo> getHistoricalProcessExitReasons( + String packageName, int pid, int maxNum, int userId) { + enforceNotIsolatedCaller("getHistoricalProcessExitReasons"); + + // For the simplification, we don't support USER_ALL nor USER_CURRENT here. + if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) { + throw new IllegalArgumentException("Unsupported userId"); + } + + final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandle.getCallingUserId(); + mUserController.handleIncomingUser(callingPid, callingUid, userId, true, ALLOW_NON_FULL, + "getHistoricalProcessExitReasons", null); + + final ArrayList<ApplicationExitInfo> results = new ArrayList<ApplicationExitInfo>(); + if (!TextUtils.isEmpty(packageName)) { + final int uid = enforceDumpPermissionForPackage(packageName, userId, callingUid, + "getHistoricalProcessExitReasons"); + if (uid != Process.INVALID_UID) { + mProcessList.mAppExitInfoTracker.getExitInfo( + packageName, uid, pid, maxNum, results); + } + } else { + // If no package name is given, use the caller's uid as the filter uid. + mProcessList.mAppExitInfoTracker.getExitInfo( + packageName, callingUid, pid, maxNum, results); + } + + return new ParceledListSlice<ApplicationExitInfo>(results); + } + + /** + * Check if the calling process has the permission to dump given package, + * throw SecurityException if it doesn't have the permission. + * + * @return The UID of the given package, or {@link android.os.Process#INVALID_UID} + * if the package is not found. + */ + private int enforceDumpPermissionForPackage(String packageName, int userId, int callingUid, + String function) { + long identity = Binder.clearCallingIdentity(); + int uid = Process.INVALID_UID; + try { + uid = mPackageManagerInt.getPackageUid(packageName, + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, userId); + } finally { + Binder.restoreCallingIdentity(identity); + } + if (uid == Process.INVALID_UID) { + return Process.INVALID_UID; + } + if (UserHandle.getAppId(uid) != UserHandle.getAppId(callingUid)) { + // Requires the DUMP permission if the target package doesn't belong + // to the caller. + enforceCallingPermission(android.Manifest.permission.DUMP, function); + } + return uid; + } + + @Override public void getMyMemoryState(ActivityManager.RunningAppProcessInfo outState) { if (outState == null) { throw new IllegalArgumentException("outState is null"); @@ -10150,6 +10222,12 @@ public class ActivityManagerService extends IActivityManager.Stub if (dumpAll) { pw.println("-------------------------------------------------------------------------------"); } + mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage); + pw.println(); + if (dumpAll) { + pw.println("-------------------------------------------------------------------" + + "------------"); + } dumpProcessesLocked(fd, pw, args, opti, dumpAll, dumpPackage, dumpAppId); pw.println(); if (dumpAll) { @@ -10448,6 +10526,12 @@ public class ActivityManagerService extends IActivityManager.Stub synchronized (this) { dumpUsersLocked(pw); } + } else if ("exit-info".equals(cmd)) { + if (opti < args.length) { + dumpPackage = args[opti]; + opti++; + } + mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage); } else { // Dumping a single activity? if (!mAtmInternal.dumpActivity(fd, pw, cmd, args, opti, dumpAll, @@ -14061,7 +14145,9 @@ public class ActivityManagerService extends IActivityManager.Stub capp.kill("depends on provider " + cpr.name.flattenToShortString() + " in dying proc " + (proc != null ? proc.processName : "??") - + " (adj " + (proc != null ? proc.setAdj : "??") + ")", true); + + " (adj " + (proc != null ? proc.setAdj : "??") + ")", + ApplicationExitInfo.REASON_OTHER, + true); } } else if (capp.thread != null && conn.provider.provider != null) { try { @@ -14146,6 +14232,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Take care of any launching providers waiting for this process. if (cleanupAppInLaunchingProvidersLocked(app, false)) { + mProcessList.noteProcessDiedLocked(app); restart = true; } @@ -14242,6 +14329,7 @@ public class ActivityManagerService extends IActivityManager.Stub mProcessesOnHold.remove(app); mAtmInternal.onCleanUpApplicationRecord(app.getWindowProcessController()); + mProcessList.noteProcessDiedLocked(app); if (restart && !app.isolated) { // We have components that still need to be running in the @@ -16968,7 +17056,10 @@ public class ActivityManagerService extends IActivityManager.Stub uptimeSince, cputimeUsed); } app.kill("excessive cpu " + cputimeUsed + " during " + uptimeSince - + " dur=" + checkDur + " limit=" + cpuLimit, true); + + " dur=" + checkDur + " limit=" + cpuLimit, + ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE, + ApplicationExitInfo.SUBREASON_EXCESSIVE_CPU, + true); app.baseProcessTracker.reportExcessiveCpu(app.pkgList.mPkgList); for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) { ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg); @@ -17602,7 +17693,10 @@ public class ActivityManagerService extends IActivityManager.Stub + (app.thread != null ? app.thread.asBinder() : null) + ")\n"); if (app.pid > 0 && app.pid != MY_PID) { - app.kill("empty", false); + app.kill("empty", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_TRIM_EMPTY, + false); } else if (app.thread != null) { try { app.thread.scheduleExit(); @@ -18432,7 +18526,7 @@ public class ActivityManagerService extends IActivityManager.Stub final ProcessRecord pr = (ProcessRecord) wpc.mOwner; if (pr.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND && pr.curReceivers.isEmpty()) { - pr.kill("remove task", true); + pr.kill("remove task", ApplicationExitInfo.REASON_OTHER, true); } else { // We delay killing processes that are not in the background or running a // receiver. @@ -19237,6 +19331,13 @@ public class ActivityManagerService extends IActivityManager.Stub return false; } + /** + * Return the process list instance + */ + public ProcessList getProcessList(ActivityManagerService service) { + return new ProcessList(); + } + private boolean ensureHasNetworkManagementInternal() { if (mNmi == null) { mNmi = LocalServices.getService(NetworkManagementInternal.class); diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index d7ad1c252401..e7d6eb7bb46b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -91,7 +91,6 @@ import android.view.Display; import com.android.internal.compat.CompatibilityChangeConfig; import com.android.internal.util.HexDump; import com.android.internal.util.MemInfoReader; -import com.android.internal.util.Preconditions; import com.android.server.compat.PlatformCompat; import java.io.BufferedReader; @@ -217,6 +216,8 @@ final class ActivityManagerShellCommand extends ShellCommand { return runSetWatchHeap(pw); case "clear-watch-heap": return runClearWatchHeap(pw); + case "clear-exit-info": + return runClearExitInfo(pw); case "bug-report": return runBugReport(pw); case "force-stop": @@ -1019,6 +1020,30 @@ final class ActivityManagerShellCommand extends ShellCommand { return 0; } + int runClearExitInfo(PrintWriter pw) throws RemoteException { + mInternal.enforceCallingPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS, + "runClearExitInfo()"); + String opt; + int userId = UserHandle.USER_CURRENT; + String packageName = null; + while ((opt = getNextOption()) != null) { + if (opt.equals("--user")) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } else { + packageName = opt; + } + } + if (userId == UserHandle.USER_CURRENT) { + UserInfo user = mInterface.getCurrentUser(); + if (user == null) { + return -1; + } + userId = user.id; + } + mInternal.mProcessList.mAppExitInfoTracker.clearHistoryProcessExitInfo(packageName, userId); + return 0; + } + int runBugReport(PrintWriter pw) throws RemoteException { String opt; boolean fullBugreport = true; @@ -1145,7 +1170,7 @@ final class ActivityManagerShellCommand extends ShellCommand { static final int RESULT_ANR_DIALOG = 0; static final int RESULT_ANR_KILL = 1; - static final int RESULT_ANR_WAIT = 1; + static final int RESULT_ANR_WAIT = 2; int mResult; @@ -3008,6 +3033,7 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" s[ervices] [COMP_SPEC ...]: service state"); pw.println(" allowed-associations: current package association restrictions"); pw.println(" as[sociations]: tracked app associations"); + pw.println(" exit-info [PACKAGE_NAME]: historical process exit information"); pw.println(" lmk: stats on low memory killer"); pw.println(" lru: raw LRU process list"); pw.println(" binder-proxies: stats on binder objects and IPCs"); @@ -3142,6 +3168,8 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" above <HEAP-LIMIT> then a heap dump is collected for the user to report."); pw.println(" clear-watch-heap"); pw.println(" Clear the previously set-watch-heap."); + pw.println(" clear-exit-info [--user <USER_ID> | all | current] [package]"); + pw.println(" Clear the process exit-info for given package"); pw.println(" bug-report [--progress | --telephony]"); pw.println(" Request bug report generation; will launch a notification"); pw.println(" when done to select where it should be delivered. Options are:"); diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 8071f52037e6..145f91bdb0b3 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -29,6 +29,7 @@ import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_N import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ApplicationErrorReport; +import android.app.ApplicationExitInfo; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -315,11 +316,23 @@ class AppErrors { } void killAppAtUserRequestLocked(ProcessRecord app) { - app.getDialogController().clearAllErrorDialogs(); - killAppImmediateLocked(app, "user-terminated", "user request after error"); + ProcessRecord.ErrorDialogController controller = + app.getDialogController(); + + int reasonCode = ApplicationExitInfo.REASON_ANR; + int subReason = ApplicationExitInfo.SUBREASON_UNKNOWN; + if (controller.hasDebugWaitingDialog()) { + reasonCode = ApplicationExitInfo.REASON_OTHER; + subReason = ApplicationExitInfo.SUBREASON_WAIT_FOR_DEBUGGER; + } + + controller.clearAllErrorDialogs(); + killAppImmediateLocked(app, reasonCode, subReason, + "user-terminated", "user request after error"); } - private void killAppImmediateLocked(ProcessRecord app, String reason, String killReason) { + private void killAppImmediateLocked(ProcessRecord app, int reasonCode, int subReason, + String reason, String killReason) { app.setCrashing(false); app.crashingReport = null; app.setNotResponding(false); @@ -327,7 +340,7 @@ class AppErrors { if (app.pid > 0 && app.pid != MY_PID) { handleAppCrashLocked(app, reason, null /*shortMsg*/, null /*longMsg*/, null /*stackTrace*/, null /*data*/); - app.kill(killReason, true); + app.kill(killReason, reasonCode, subReason, true); } } @@ -381,7 +394,9 @@ class AppErrors { mService.mHandler.postDelayed( () -> { synchronized (mService) { - killAppImmediateLocked(p, "forced", "killed for invalid state"); + killAppImmediateLocked(p, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, + "forced", "killed for invalid state"); } }, 5000L); @@ -422,6 +437,13 @@ class AppErrors { if (r != null) { mPackageWatchdog.onPackageFailure(r.getPackageListWithVersionCode(), PackageWatchdog.FAILURE_REASON_APP_CRASH); + + mService.mProcessList.noteAppKill(r, (crashInfo != null + && "Native crash".equals(crashInfo.exceptionClassName)) + ? ApplicationExitInfo.REASON_CRASH_NATIVE + : ApplicationExitInfo.REASON_CRASH, + ApplicationExitInfo.SUBREASON_UNKNOWN, + "crash"); } final int relaunchReason = r != null @@ -488,7 +510,8 @@ class AppErrors { stopReportingCrashesLocked(r); } if (res == AppErrorDialog.RESTART) { - mService.mProcessList.removeProcessLocked(r, false, true, "crash"); + mService.mProcessList.removeProcessLocked(r, false, true, "crash", + ApplicationExitInfo.REASON_CRASH); if (taskId != INVALID_TASK_ID) { try { mService.startActivityFromRecents(taskId, @@ -506,7 +529,8 @@ class AppErrors { // Kill it with fire! mService.mAtmInternal.onHandleAppCrash(r.getWindowProcessController()); if (!r.isPersistent()) { - mService.mProcessList.removeProcessLocked(r, false, false, "crash"); + mService.mProcessList.removeProcessLocked(r, false, false, "crash", + ApplicationExitInfo.REASON_CRASH); mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */); } } finally { @@ -549,22 +573,26 @@ class AppErrors { return mService.mAtmInternal.handleAppCrashInActivityController( name, pid, shortMsg, longMsg, timeMillis, crashInfo.stackTrace, () -> { - if ("1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0")) - && "Native crash".equals(crashInfo.exceptionClassName)) { - Slog.w(TAG, "Skip killing native crashed app " + name - + "(" + pid + ") during testing"); - } else { - Slog.w(TAG, "Force-killing crashed app " + name + " at watcher's request"); - if (r != null) { - if (!makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace, null)) { - r.kill("crash", true); - } + if ("1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0")) + && "Native crash".equals(crashInfo.exceptionClassName)) { + Slog.w(TAG, "Skip killing native crashed app " + name + + "(" + pid + ") during testing"); } else { - // Huh. - Process.killProcess(pid); - ProcessList.killProcessGroup(uid, pid); + Slog.w(TAG, "Force-killing crashed app " + name + " at watcher's request"); + if (r != null) { + if (!makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace, null)) { + r.kill("crash", ApplicationExitInfo.REASON_CRASH, true); + } + } else { + // Huh. + Process.killProcess(pid); + ProcessList.killProcessGroup(uid, pid); + mService.mProcessList.noteAppKill(pid, uid, + ApplicationExitInfo.REASON_CRASH, + ApplicationExitInfo.SUBREASON_UNKNOWN, + "crash"); + } } - } }); } @@ -719,7 +747,8 @@ class AppErrors { // Don't let services in this process be restarted and potentially // annoy the user repeatedly. Unless it is persistent, since those // processes run critical code. - mService.mProcessList.removeProcessLocked(app, false, tryAgain, "crash"); + mService.mProcessList.removeProcessLocked(app, false, tryAgain, "crash", + ApplicationExitInfo.REASON_CRASH); mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */); if (!showBackground) { return false; diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java new file mode 100644 index 000000000000..6e135d65f127 --- /dev/null +++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java @@ -0,0 +1,1302 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static android.app.ActivityManager.RunningAppProcessInfo.procStateToImportance; + +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; + +import android.app.ApplicationExitInfo; +import android.app.ApplicationExitInfo.Reason; +import android.app.ApplicationExitInfo.SubReason; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.icu.text.SimpleDateFormat; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.system.OsConstants; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Pair; +import android.util.Pools.SynchronizedPool; +import android.util.Slog; +import android.util.SparseArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ProcessMap; +import com.android.server.IoThread; +import com.android.server.SystemServiceManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * A class to manage all the {@link android.app.ApplicationExitInfo} records. + */ +public final class AppExitInfoTracker { + private static final String TAG = TAG_WITH_CLASS_NAME ? "AppExitInfoTracker" : TAG_AM; + + /** + * Interval of persisting the app exit info to persistent storage. + */ + private static final long APP_EXIT_INFO_PERSIST_INTERVAL = TimeUnit.MINUTES.toMillis(30); + + /** These are actions that the forEachPackage should take after each iteration */ + private static final int FOREACH_ACTION_NONE = 0; + private static final int FOREACH_ACTION_REMOVE_PACKAGE = 1; + private static final int FOREACH_ACTION_STOP_ITERATION = 2; + + private static final int APP_EXIT_RAW_INFO_POOL_SIZE = 8; + + @VisibleForTesting + static final String APP_EXIT_INFO_FILE = "procexitinfo"; + + private final Object mLock = new Object(); + + /** + * Initialized in {@link #init} and read-only after that. + */ + private ActivityManagerService mService; + + /** + * Initialized in {@link #init} and read-only after that. + */ + private KillHandler mKillHandler; + + /** + * The task to persist app process exit info + */ + @GuardedBy("mLock") + private Runnable mAppExitInfoPersistTask = null; + + /** + * Last time(in ms) since epoch that the app exit info was persisted into persistent storage. + */ + @GuardedBy("mLock") + private long mLastAppExitInfoPersistTimestamp = 0L; + + /** + * Retention policy: keep up to X historical exit info per package. + * + * Initialized in {@link #init} and read-only after that. + * Not lock is needed. + */ + private int mAppExitInfoHistoryListSize; + + /* + * PackageName/uid -> [pid/info, ...] holder, the uid here is the package uid. + */ + @GuardedBy("mLock") + private final ProcessMap<AppExitInfoContainer> mData; + + /** A pool of raw {@link android.app.ApplicationExitInfo} records. */ + @GuardedBy("mService") + private final SynchronizedPool<ApplicationExitInfo> mRawRecordsPool; + + /** + * Wheather or not we've loaded the historical app process exit info from + * persistent storage. + */ + @VisibleForTesting + @GuardedBy("mLock") + boolean mAppExitInfoLoaded = false; + + /** + * Temporary list being used to filter/sort intermediate results in {@link #getExitInfo}. + */ + @GuardedBy("mLock") + final ArrayList<ApplicationExitInfo> mTmpInfoList = new ArrayList<ApplicationExitInfo>(); + + /** + * Temporary list being used to filter/sort intermediate results in {@link #getExitInfo}. + */ + @GuardedBy("mLock") + final ArrayList<ApplicationExitInfo> mTmpInfoList2 = new ArrayList<ApplicationExitInfo>(); + + /** + * The path to the historical proc exit info file, persisted in the storage. + */ + @VisibleForTesting + File mProcExitInfoFile; + + /** + * Mapping between the isolated UID to its application uid. + */ + final IsolatedUidRecords mIsolatedUidRecords = + new IsolatedUidRecords(); + + /** + * Bookkeeping app process exit info from Zygote. + */ + final AppExitInfoExternalSource mAppExitInfoSourceZygote = + new AppExitInfoExternalSource("zygote", null); + + /** + * Bookkeeping low memory kills info from lmkd. + */ + final AppExitInfoExternalSource mAppExitInfoSourceLmkd = + new AppExitInfoExternalSource("lmkd", ApplicationExitInfo.REASON_LOW_MEMORY); + + AppExitInfoTracker() { + mData = new ProcessMap<AppExitInfoContainer>(); + mRawRecordsPool = new SynchronizedPool<ApplicationExitInfo>(APP_EXIT_RAW_INFO_POOL_SIZE); + } + + void init(ActivityManagerService service, Looper looper) { + mService = service; + mKillHandler = new KillHandler(looper); + mProcExitInfoFile = new File(SystemServiceManager.ensureSystemDir(), APP_EXIT_INFO_FILE); + + mAppExitInfoHistoryListSize = service.mContext.getResources().getInteger( + com.android.internal.R.integer.config_app_exit_info_history_list_size); + } + + void onSystemReady() { + // Read the sysprop set by lmkd and set this to persist so app could read it. + SystemProperties.set("persist.sys.lmk.reportkills", + Boolean.toString(SystemProperties.getBoolean("sys.lmk.reportkills", false))); + registerForUserRemoval(); + registerForPackageRemoval(); + IoThread.getHandler().post(this::loadExistingProcessExitInfo); + } + + @GuardedBy("mService") + void scheduleNoteProcessDiedLocked(final ProcessRecord app) { + if (app == null || app.info == null) { + return; + } + + synchronized (mLock) { + if (!mAppExitInfoLoaded) { + return; + } + } + // The current thread is holding the global lock, let's extract the info from it + // and schedule the info note task in the kill handler. + mKillHandler.obtainMessage(KillHandler.MSG_PROC_DIED, obtainRawRecordLocked(app)) + .sendToTarget(); + } + + void scheduleNoteAppKill(final ProcessRecord app, final @Reason int reason, + final @SubReason int subReason, final String msg) { + synchronized (mLock) { + if (!mAppExitInfoLoaded) { + return; + } + } + synchronized (mService) { + if (app == null || app.info == null) { + return; + } + + ApplicationExitInfo raw = obtainRawRecordLocked(app); + raw.setReason(reason); + raw.setSubReason(subReason); + raw.setDescription(msg); + mKillHandler.obtainMessage(KillHandler.MSG_APP_KILL, raw).sendToTarget(); + } + } + + void scheduleNoteAppKill(final int pid, final int uid, final @Reason int reason, + final @SubReason int subReason, final String msg) { + synchronized (mLock) { + if (!mAppExitInfoLoaded) { + return; + } + } + synchronized (mService) { + ProcessRecord app; + synchronized (mService.mPidsSelfLocked) { + app = mService.mPidsSelfLocked.get(pid); + } + if (app == null) { + if (DEBUG_PROCESSES) { + Slog.w(TAG, "Skipping saving the kill reason for pid " + pid + + "(uid=" + uid + ") since its process record is not found"); + } + } else { + scheduleNoteAppKill(app, reason, subReason, msg); + } + } + } + + interface LmkdKillListener { + /** + * Called when there is a process kill by lmkd. + */ + void onLmkdKillOccurred(int pid, int uid); + } + + void setLmkdKillListener(final LmkdKillListener listener) { + synchronized (mLock) { + mAppExitInfoSourceLmkd.setOnProcDiedListener((pid, uid) -> + listener.onLmkdKillOccurred(pid, uid)); + } + } + + /** Called when there is a low memory kill */ + void scheduleNoteLmkdProcKilled(final int pid, final int uid) { + mKillHandler.obtainMessage(KillHandler.MSG_LMKD_PROC_KILLED, pid, uid) + .sendToTarget(); + } + + private void scheduleChildProcDied(int pid, int uid, int status) { + mKillHandler.obtainMessage(KillHandler.MSG_CHILD_PROC_DIED, pid, uid, (Integer) status) + .sendToTarget(); + } + + /** Calls when zygote sends us SIGCHLD */ + void handleZygoteSigChld(int pid, int uid, int status) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, "Got SIGCHLD from zygote: pid=" + pid + ", uid=" + uid + + ", status=" + Integer.toHexString(status)); + } + scheduleChildProcDied(pid, uid, status); + } + + /** + * Main routine to create or update the {@link android.app.ApplicationExitInfo} for the given + * ProcessRecord, also query the zygote and lmkd records to make the information more accurate. + */ + @VisibleForTesting + @GuardedBy("mLock") + void handleNoteProcessDiedLocked(final ApplicationExitInfo raw) { + if (raw != null) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, "Update process exit info for " + raw.getPackageName() + + "(" + raw.getPid() + "/u" + raw.getRealUid() + ")"); + } + + ApplicationExitInfo info = getExitInfo(raw.getPackageName(), + raw.getPackageUid(), raw.getPid()); + + // query zygote and lmkd to get the exit info, and clear the saved info + Pair<Long, Object> zygote = mAppExitInfoSourceZygote.remove( + raw.getPid(), raw.getRealUid()); + Pair<Long, Object> lmkd = mAppExitInfoSourceLmkd.remove( + raw.getPid(), raw.getRealUid()); + mIsolatedUidRecords.removeIsolatedUid(raw.getRealUid()); + + if (info == null) { + info = addExitInfoLocked(raw); + } + + if (lmkd != null) { + updateExistingExitInfoRecordLocked(info, null, + ApplicationExitInfo.REASON_LOW_MEMORY); + } else if (zygote != null) { + updateExistingExitInfoRecordLocked(info, (Integer) zygote.second, null); + } + } + } + + /** + * Make note when ActivityManagerService decides to kill an application process. + */ + @VisibleForTesting + @GuardedBy("mLock") + void handleNoteAppKillLocked(final ApplicationExitInfo raw) { + ApplicationExitInfo info = getExitInfo( + raw.getPackageName(), raw.getPackageUid(), raw.getPid()); + + if (info == null) { + addExitInfoLocked(raw); + } else { + // always override the existing info since we are now more informational. + info.setReason(raw.getReason()); + info.setStatus(0); + info.setTimestamp(System.currentTimeMillis()); + info.setDescription(raw.getDescription()); + } + } + + @GuardedBy("mLock") + private ApplicationExitInfo addExitInfoLocked(ApplicationExitInfo raw) { + if (!mAppExitInfoLoaded) { + Slog.w(TAG, "Skipping saving the exit info due to ongoing loading from storage"); + return null; + } + + final ApplicationExitInfo info = new ApplicationExitInfo(raw); + final String[] packages = raw.getPackageList(); + final int uid = raw.getPackageUid(); + for (int i = 0; i < packages.length; i++) { + addExitInfoInner(packages[i], uid, info); + } + + schedulePersistProcessExitInfo(false); + + return info; + } + + /** + * Update an existing {@link android.app.ApplicationExitInfo} record with given information. + */ + @GuardedBy("mLock") + private void updateExistingExitInfoRecordLocked(ApplicationExitInfo info, + Integer status, Integer reason) { + if (info == null || !isFresh(info.getTimestamp())) { + // if the record is way outdated, don't update it then (because of potential pid reuse) + return; + } + if (status != null) { + if (OsConstants.WIFEXITED(status)) { + info.setReason(ApplicationExitInfo.REASON_EXIT_SELF); + info.setStatus(OsConstants.WEXITSTATUS(status)); + } else if (OsConstants.WIFSIGNALED(status)) { + if (info.getReason() == ApplicationExitInfo.REASON_UNKNOWN) { + info.setReason(ApplicationExitInfo.REASON_SIGNALED); + info.setStatus(OsConstants.WTERMSIG(status)); + } else if (info.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) { + info.setStatus(OsConstants.WTERMSIG(status)); + } + } + } + if (reason != null) { + info.setReason(reason); + } + } + + /** + * Update an existing {@link android.app.ApplicationExitInfo} record with given information. + * + * @return true if a recond is updated + */ + private boolean updateExitInfoIfNecessary(int pid, int uid, Integer status, Integer reason) { + synchronized (mLock) { + if (UserHandle.isIsolated(uid)) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + } + ArrayList<ApplicationExitInfo> tlist = mTmpInfoList; + tlist.clear(); + final int targetUid = uid; + forEachPackage((packageName, records) -> { + AppExitInfoContainer container = records.get(targetUid); + if (container == null) { + return FOREACH_ACTION_NONE; + } + tlist.clear(); + container.getExitInfoLocked(pid, 1, tlist); + if (tlist.size() == 0) { + return FOREACH_ACTION_NONE; + } + ApplicationExitInfo info = tlist.get(0); + if (info.getRealUid() != targetUid) { + tlist.clear(); + return FOREACH_ACTION_NONE; + } + // Okay found it, update its reason. + updateExistingExitInfoRecordLocked(info, status, reason); + + return FOREACH_ACTION_STOP_ITERATION; + }); + return tlist.size() > 0; + } + } + + /** + * Get the exit info with matching package name, filterUid and filterPid (if > 0) + */ + @VisibleForTesting + void getExitInfo(final String packageName, final int filterUid, + final int filterPid, final int maxNum, final ArrayList<ApplicationExitInfo> results) { + synchronized (mLock) { + boolean emptyPackageName = TextUtils.isEmpty(packageName); + if (!emptyPackageName) { + // fast path + AppExitInfoContainer container = mData.get(packageName, filterUid); + if (container != null) { + container.getExitInfoLocked(filterPid, maxNum, results); + } + } else { + // slow path + final ArrayList<ApplicationExitInfo> list = mTmpInfoList2; + list.clear(); + // get all packages + forEachPackage((name, records) -> { + AppExitInfoContainer container = records.get(filterUid); + if (container != null) { + mTmpInfoList.clear(); + results.addAll(container.toListLocked(mTmpInfoList, filterPid)); + } + return AppExitInfoTracker.FOREACH_ACTION_NONE; + }); + + Collections.sort(list, (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); + int size = list.size(); + if (maxNum > 0) { + size = Math.min(size, maxNum); + } + for (int i = 0; i < size; i++) { + results.add(list.get(i)); + } + list.clear(); + } + } + } + + /** + * Return the first matching exit info record, for internal use, the parameters are not supposed + * to be empty. + */ + private ApplicationExitInfo getExitInfo(final String packageName, + final int filterUid, final int filterPid) { + synchronized (mLock) { + ArrayList<ApplicationExitInfo> list = mTmpInfoList; + list.clear(); + getExitInfo(packageName, filterUid, filterPid, 1, list); + + ApplicationExitInfo info = list.size() > 0 ? list.get(0) : null; + list.clear(); + return info; + } + } + + @VisibleForTesting + void onUserRemoved(int userId) { + mAppExitInfoSourceZygote.removeByUserId(userId); + mAppExitInfoSourceLmkd.removeByUserId(userId); + mIsolatedUidRecords.removeByUserId(userId); + removeByUserId(userId); + schedulePersistProcessExitInfo(true); + } + + @VisibleForTesting + void onPackageRemoved(String packageName, int uid, boolean allUsers) { + if (packageName != null) { + mAppExitInfoSourceZygote.removeByUid(uid, allUsers); + mAppExitInfoSourceLmkd.removeByUid(uid, allUsers); + mIsolatedUidRecords.removeAppUid(uid, allUsers); + removePackage(packageName, allUsers ? UserHandle.USER_ALL : UserHandle.getUserId(uid)); + schedulePersistProcessExitInfo(true); + } + } + + private void registerForUserRemoval() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_REMOVED); + mService.mContext.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId < 1) return; + onUserRemoved(userId); + } + }, filter, null, mKillHandler); + } + + private void registerForPackageRemoval() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mService.mContext.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int uid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL); + boolean allUsers = intent.getBooleanExtra( + Intent.EXTRA_REMOVED_FOR_ALL_USERS, false); + onPackageRemoved(intent.getData().getSchemeSpecificPart(), uid, allUsers); + } + }, filter, null, mKillHandler); + } + + /** + * Load the existing {@link android.app.ApplicationExitInfo} records from persistent storage. + */ + @VisibleForTesting + void loadExistingProcessExitInfo() { + if (!mProcExitInfoFile.canRead()) { + synchronized (mLock) { + mAppExitInfoLoaded = true; + } + return; + } + + FileInputStream fin = null; + try { + AtomicFile af = new AtomicFile(mProcExitInfoFile); + fin = af.openRead(); + ProtoInputStream proto = new ProtoInputStream(fin); + for (int next = proto.nextField(); + next != ProtoInputStream.NO_MORE_FIELDS; + next = proto.nextField()) { + switch (next) { + case (int) AppsExitInfoProto.LAST_UPDATE_TIMESTAMP: + synchronized (mLock) { + mLastAppExitInfoPersistTimestamp = + proto.readLong(AppsExitInfoProto.LAST_UPDATE_TIMESTAMP); + } + break; + case (int) AppsExitInfoProto.PACKAGES: + loadPackagesFromProto(proto, next); + break; + } + } + } catch (IOException | IllegalArgumentException | WireTypeMismatchException e) { + Slog.w(TAG, "Error in loading historical app exit info from persistent storage: " + e); + } finally { + if (fin != null) { + try { + fin.close(); + } catch (IOException e) { + } + } + } + synchronized (mLock) { + mAppExitInfoLoaded = true; + } + } + + private void loadPackagesFromProto(ProtoInputStream proto, long fieldId) + throws IOException, WireTypeMismatchException { + long token = proto.start(fieldId); + String pkgName = ""; + for (int next = proto.nextField(); + next != ProtoInputStream.NO_MORE_FIELDS; + next = proto.nextField()) { + switch (next) { + case (int) AppsExitInfoProto.Package.PACKAGE_NAME: + pkgName = proto.readString(AppsExitInfoProto.Package.PACKAGE_NAME); + break; + case (int) AppsExitInfoProto.Package.USERS: + AppExitInfoContainer container = new AppExitInfoContainer( + mAppExitInfoHistoryListSize); + int uid = container.readFromProto(proto, AppsExitInfoProto.Package.USERS); + synchronized (mLock) { + mData.put(pkgName, uid, container); + } + break; + } + } + proto.end(token); + } + + /** + * Persist the existing {@link android.app.ApplicationExitInfo} records to storage. + */ + @VisibleForTesting + void persistProcessExitInfo() { + AtomicFile af = new AtomicFile(mProcExitInfoFile); + FileOutputStream out = null; + long now = System.currentTimeMillis(); + try { + out = af.startWrite(); + ProtoOutputStream proto = new ProtoOutputStream(out); + proto.write(AppsExitInfoProto.LAST_UPDATE_TIMESTAMP, now); + synchronized (mLock) { + forEachPackage((packageName, records) -> { + long token = proto.start(AppsExitInfoProto.PACKAGES); + proto.write(AppsExitInfoProto.Package.PACKAGE_NAME, packageName); + int uidArraySize = records.size(); + for (int j = 0; j < uidArraySize; j++) { + records.valueAt(j).writeToProto(proto, AppsExitInfoProto.Package.USERS); + } + proto.end(token); + return AppExitInfoTracker.FOREACH_ACTION_NONE; + }); + mLastAppExitInfoPersistTimestamp = now; + } + proto.flush(); + af.finishWrite(out); + } catch (IOException e) { + Slog.w(TAG, "Unable to write historical app exit info into persistent storage: " + e); + af.failWrite(out); + } + synchronized (mLock) { + mAppExitInfoPersistTask = null; + } + } + + /** + * Schedule a task to persist the {@link android.app.ApplicationExitInfo} records to storage. + */ + @VisibleForTesting + void schedulePersistProcessExitInfo(boolean immediately) { + synchronized (mLock) { + if (mAppExitInfoPersistTask == null || immediately) { + if (mAppExitInfoPersistTask != null) { + IoThread.getHandler().removeCallbacks(mAppExitInfoPersistTask); + } + mAppExitInfoPersistTask = this::persistProcessExitInfo; + IoThread.getHandler().postDelayed(mAppExitInfoPersistTask, + immediately ? 0 : APP_EXIT_INFO_PERSIST_INTERVAL); + } + } + } + + /** + * Helper function for testing only. + */ + @VisibleForTesting + void clearProcessExitInfo(boolean removeFile) { + synchronized (mLock) { + if (mAppExitInfoPersistTask != null) { + IoThread.getHandler().removeCallbacks(mAppExitInfoPersistTask); + mAppExitInfoPersistTask = null; + } + if (removeFile && mProcExitInfoFile != null) { + mProcExitInfoFile.delete(); + } + mData.getMap().clear(); + } + } + + /** + * Helper function for shell command + */ + void clearHistoryProcessExitInfo(String packageName, int userId) { + synchronized (mLock) { + if (TextUtils.isEmpty(packageName)) { + if (userId == UserHandle.USER_ALL) { + mData.getMap().clear(); + } else { + removeByUserId(userId); + } + } else { + removePackage(packageName, userId); + } + } + schedulePersistProcessExitInfo(true); + } + + void dumpHistoryProcessExitInfo(PrintWriter pw, String packageName) { + pw.println("ACTIVITY MANAGER LRU PROCESSES (dumpsys activity exit-info)"); + SimpleDateFormat sdf = new SimpleDateFormat(); + synchronized (mLock) { + pw.println("Last Timestamp of Persistence Into Persistent Storage: " + + sdf.format(new Date(mLastAppExitInfoPersistTimestamp))); + if (TextUtils.isEmpty(packageName)) { + forEachPackage((name, records) -> { + dumpHistoryProcessExitInfoLocked(pw, " ", name, records, sdf); + return AppExitInfoTracker.FOREACH_ACTION_NONE; + }); + } else { + SparseArray<AppExitInfoContainer> array = mData.getMap().get(packageName); + if (array != null) { + dumpHistoryProcessExitInfoLocked(pw, " ", packageName, array, sdf); + } + } + } + } + + @GuardedBy("mLock") + private void dumpHistoryProcessExitInfoLocked(PrintWriter pw, String prefix, + String packageName, SparseArray<AppExitInfoContainer> array, + SimpleDateFormat sdf) { + pw.println(prefix + "package: " + packageName); + int size = array.size(); + for (int i = 0; i < size; i++) { + pw.println(prefix + " Historical Process Exit for userId=" + array.keyAt(i)); + array.valueAt(i).dumpLocked(pw, prefix + " ", sdf); + } + } + + private void addExitInfoInner(String packageName, int userId, ApplicationExitInfo info) { + synchronized (mLock) { + AppExitInfoContainer container = mData.get(packageName, userId); + if (container == null) { + container = new AppExitInfoContainer(mAppExitInfoHistoryListSize); + if (UserHandle.isIsolated(info.getRealUid())) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(info.getRealUid()); + if (k != null) { + container.mUid = k; + } + } else { + container.mUid = info.getRealUid(); + } + mData.put(packageName, userId, container); + } + container.addExitInfoLocked(info); + } + } + + private void forEachPackage( + BiFunction<String, SparseArray<AppExitInfoContainer>, Integer> callback) { + if (callback != null) { + synchronized (mLock) { + ArrayMap<String, SparseArray<AppExitInfoContainer>> map = mData.getMap(); + for (int i = map.size() - 1; i >= 0; i--) { + switch (callback.apply(map.keyAt(i), map.valueAt(i))) { + case FOREACH_ACTION_REMOVE_PACKAGE: + map.removeAt(i); + break; + case FOREACH_ACTION_STOP_ITERATION: + i = 0; + break; + case FOREACH_ACTION_NONE: + default: + break; + } + } + } + } + } + + private void removePackage(String packageName, int userId) { + synchronized (mLock) { + if (userId == UserHandle.USER_ALL) { + mData.getMap().remove(packageName); + } else { + ArrayMap<String, SparseArray<AppExitInfoContainer>> map = + mData.getMap(); + SparseArray<AppExitInfoContainer> array = map.get(packageName); + if (array == null) { + return; + } + for (int i = array.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(array.keyAt(i)) == userId) { + array.removeAt(i); + break; + } + } + if (array.size() == 0) { + map.remove(packageName); + } + } + } + } + + private void removeByUserId(final int userId) { + if (userId == UserHandle.USER_ALL) { + synchronized (mLock) { + mData.getMap().clear(); + } + return; + } + forEachPackage((packageName, records) -> { + for (int i = records.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(records.keyAt(i)) == userId) { + records.removeAt(i); + break; + } + } + return records.size() == 0 ? FOREACH_ACTION_REMOVE_PACKAGE : FOREACH_ACTION_NONE; + }); + } + + @VisibleForTesting + @GuardedBy("mService") + ApplicationExitInfo obtainRawRecordLocked(ProcessRecord app) { + ApplicationExitInfo info = mRawRecordsPool.acquire(); + if (info == null) { + info = new ApplicationExitInfo(); + } + + final int definingUid = app.hostingRecord != null ? app.hostingRecord.getDefiningUid() : 0; + info.setPid(app.pid); + info.setRealUid(app.uid); + info.setPackageUid(app.info.uid); + info.setDefiningUid(definingUid > 0 ? definingUid : app.info.uid); + info.setProcessName(app.processName); + info.setConnectionGroup(app.connectionGroup); + info.setPackageName(app.info.packageName); + info.setPackageList(app.getPackageList()); + info.setReason(ApplicationExitInfo.REASON_UNKNOWN); + info.setStatus(0); + info.setImportance(procStateToImportance(app.setProcState)); + info.setPss(app.lastMemInfo == null ? 0 : app.lastMemInfo.getTotalPss()); + info.setRss(app.lastMemInfo == null ? 0 : app.lastMemInfo.getTotalRss()); + info.setTimestamp(System.currentTimeMillis()); + + return info; + } + + @VisibleForTesting + @GuardedBy("mService") + void recycleRawRecordLocked(ApplicationExitInfo info) { + info.setProcessName(null); + info.setDescription(null); + info.setPackageList(null); + + mRawRecordsPool.release(info); + } + + /** + * A container class of {@link android.app.ApplicationExitInfo} + */ + final class AppExitInfoContainer { + private SparseArray<ApplicationExitInfo> mInfos; // index is pid + private int mMaxCapacity; + private int mUid; // Application uid, not isolated uid. + + AppExitInfoContainer(final int maxCapacity) { + mInfos = new SparseArray<ApplicationExitInfo>(); + mMaxCapacity = maxCapacity; + } + + @GuardedBy("mLock") + void getExitInfoLocked(final int filterPid, final int maxNum, + ArrayList<ApplicationExitInfo> results) { + if (filterPid > 0) { + ApplicationExitInfo r = mInfos.get(filterPid); + if (r != null) { + results.add(r); + } + } else { + final int numRep = mInfos.size(); + if (maxNum <= 0 || numRep <= maxNum) { + // Return all records. + for (int i = 0; i < numRep; i++) { + results.add(mInfos.valueAt(i)); + } + Collections.sort(results, + (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); + } else { + if (maxNum == 1) { + // Most of the caller might be only interested with the most recent one + ApplicationExitInfo r = mInfos.valueAt(0); + for (int i = 1; i < numRep; i++) { + ApplicationExitInfo t = mInfos.valueAt(i); + if (r.getTimestamp() < t.getTimestamp()) { + r = t; + } + } + results.add(r); + } else { + // Huh, need to sort it out then. + ArrayList<ApplicationExitInfo> list = mTmpInfoList2; + list.clear(); + for (int i = 0; i < numRep; i++) { + list.add(mInfos.valueAt(i)); + } + Collections.sort(list, + (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); + for (int i = 0; i < maxNum; i++) { + results.add(list.get(i)); + } + list.clear(); + } + } + } + } + + @GuardedBy("mLock") + void addExitInfoLocked(ApplicationExitInfo info) { + int size; + if ((size = mInfos.size()) >= mMaxCapacity) { + int oldestIndex = -1; + long oldestTimeStamp = Long.MAX_VALUE; + for (int i = 0; i < size; i++) { + ApplicationExitInfo r = mInfos.valueAt(i); + if (r.getTimestamp() < oldestTimeStamp) { + oldestTimeStamp = r.getTimestamp(); + oldestIndex = i; + } + } + if (oldestIndex >= 0) { + mInfos.removeAt(oldestIndex); + } + } + mInfos.append(info.getPid(), info); + } + + @GuardedBy("mLock") + void dumpLocked(PrintWriter pw, String prefix, SimpleDateFormat sdf) { + ArrayList<ApplicationExitInfo> list = new ArrayList<ApplicationExitInfo>(); + for (int i = mInfos.size() - 1; i >= 0; i--) { + list.add(mInfos.valueAt(i)); + } + Collections.sort(list, (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); + int size = list.size(); + for (int i = 0; i < size; i++) { + list.get(i).dump(pw, prefix + " ", "#" + i, sdf); + } + } + + @GuardedBy("mLock") + void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(AppsExitInfoProto.Package.User.UID, mUid); + int size = mInfos.size(); + for (int i = 0; i < size; i++) { + mInfos.valueAt(i).writeToProto(proto, AppsExitInfoProto.Package.User.APP_EXIT_INFO); + } + proto.end(token); + } + + int readFromProto(ProtoInputStream proto, long fieldId) + throws IOException, WireTypeMismatchException { + long token = proto.start(fieldId); + for (int next = proto.nextField(); + next != ProtoInputStream.NO_MORE_FIELDS; + next = proto.nextField()) { + switch (next) { + case (int) AppsExitInfoProto.Package.User.UID: + mUid = proto.readInt(AppsExitInfoProto.Package.User.UID); + break; + case (int) AppsExitInfoProto.Package.User.APP_EXIT_INFO: + ApplicationExitInfo info = new ApplicationExitInfo(); + info.readFromProto(proto, AppsExitInfoProto.Package.User.APP_EXIT_INFO); + mInfos.put(info.getPid(), info); + break; + } + } + proto.end(token); + return mUid; + } + + @GuardedBy("mLock") + List<ApplicationExitInfo> toListLocked(List<ApplicationExitInfo> list, int filterPid) { + if (list == null) { + list = new ArrayList<ApplicationExitInfo>(); + } + for (int i = mInfos.size() - 1; i >= 0; i--) { + if (filterPid == 0 || filterPid == mInfos.keyAt(i)) { + list.add(mInfos.valueAt(i)); + } + } + return list; + } + } + + /** + * Maintains the mapping between real UID and the application uid. + */ + final class IsolatedUidRecords { + /** + * A mapping from application uid (with the userId) to isolated uids. + */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<Integer>> mUidToIsolatedUidMap; + + /** + * A mapping from isolated uids to application uid (with the userId) + */ + @GuardedBy("mLock") + private final SparseArray<Integer> mIsolatedUidToUidMap; + + IsolatedUidRecords() { + mUidToIsolatedUidMap = new SparseArray<ArraySet<Integer>>(); + mIsolatedUidToUidMap = new SparseArray<Integer>(); + } + + void addIsolatedUid(int isolatedUid, int uid) { + synchronized (mLock) { + ArraySet<Integer> set = mUidToIsolatedUidMap.get(uid); + if (set == null) { + set = new ArraySet<Integer>(); + mUidToIsolatedUidMap.put(uid, set); + } + set.add(isolatedUid); + + mIsolatedUidToUidMap.put(isolatedUid, uid); + } + } + + Integer getUidByIsolatedUid(int isolatedUid) { + if (UserHandle.isIsolated(isolatedUid)) { + synchronized (mLock) { + return mIsolatedUidToUidMap.get(isolatedUid); + } + } + return isolatedUid; + } + + @GuardedBy("mLock") + private void removeAppUidLocked(int uid) { + ArraySet<Integer> set = mUidToIsolatedUidMap.get(uid); + if (set != null) { + for (int i = set.size() - 1; i >= 0; i--) { + int isolatedUid = set.removeAt(i); + mIsolatedUidToUidMap.remove(isolatedUid); + } + } + } + + void removeAppUid(int uid, boolean allUsers) { + synchronized (mLock) { + if (allUsers) { + uid = UserHandle.getAppId(uid); + for (int i = mUidToIsolatedUidMap.size() - 1; i >= 0; i--) { + int u = mUidToIsolatedUidMap.keyAt(i); + if (uid == UserHandle.getAppId(u)) { + removeAppUidLocked(u); + } + mUidToIsolatedUidMap.removeAt(i); + } + } else { + removeAppUidLocked(uid); + mUidToIsolatedUidMap.remove(uid); + } + } + } + + int removeIsolatedUid(int isolatedUid) { + if (!UserHandle.isIsolated(isolatedUid)) { + return isolatedUid; + } + synchronized (mLock) { + int uid = mIsolatedUidToUidMap.get(isolatedUid, -1); + if (uid == -1) { + return isolatedUid; + } + mIsolatedUidToUidMap.remove(isolatedUid); + ArraySet<Integer> set = mUidToIsolatedUidMap.get(uid); + if (set != null) { + set.remove(isolatedUid); + } + // let the ArraySet stay in the mUidToIsolatedUidMap even if it's empty + return uid; + } + } + + void removeByUserId(int userId) { + if (userId == UserHandle.USER_CURRENT) { + userId = mService.mUserController.getCurrentUserId(); + } + synchronized (mLock) { + if (userId == UserHandle.USER_ALL) { + mIsolatedUidToUidMap.clear(); + mUidToIsolatedUidMap.clear(); + return; + } + for (int i = mIsolatedUidToUidMap.size() - 1; i >= 0; i--) { + int isolatedUid = mIsolatedUidToUidMap.keyAt(i); + int uid = mIsolatedUidToUidMap.valueAt(i); + if (UserHandle.getUserId(uid) == userId) { + mIsolatedUidToUidMap.removeAt(i); + mUidToIsolatedUidMap.remove(uid); + } + } + } + } + } + + final class KillHandler extends Handler { + static final int MSG_LMKD_PROC_KILLED = 4101; + static final int MSG_CHILD_PROC_DIED = 4102; + static final int MSG_PROC_DIED = 4103; + static final int MSG_APP_KILL = 4104; + + KillHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LMKD_PROC_KILLED: + mAppExitInfoSourceLmkd.onProcDied(msg.arg1 /* pid */, msg.arg2 /* uid */, + null /* status */); + break; + case MSG_CHILD_PROC_DIED: + mAppExitInfoSourceZygote.onProcDied(msg.arg1 /* pid */, msg.arg2 /* uid */, + (Integer) msg.obj /* status */); + break; + case MSG_PROC_DIED: { + ApplicationExitInfo raw = (ApplicationExitInfo) msg.obj; + synchronized (mLock) { + handleNoteProcessDiedLocked(raw); + } + synchronized (mService) { + recycleRawRecordLocked(raw); + } + } + break; + case MSG_APP_KILL: { + ApplicationExitInfo raw = (ApplicationExitInfo) msg.obj; + synchronized (mLock) { + handleNoteAppKillLocked(raw); + } + synchronized (mService) { + recycleRawRecordLocked(raw); + } + } + break; + default: + super.handleMessage(msg); + } + } + } + + private static boolean isFresh(long timestamp) { + // A process could be dying but being stuck in some state, i.e., + // being TRACED by tombstoned, thus the zygote receives SIGCHILD + // way after we already knew the kill (maybe because we did the kill :P), + // so here check if the last known kill information is "fresh" enough. + long now = System.currentTimeMillis(); + + return (timestamp + AppExitInfoExternalSource.APP_EXIT_INFO_FRESHNESS_MS) >= now; + } + + /** + * Keep the raw information about app kills from external sources, i.e., lmkd + */ + final class AppExitInfoExternalSource { + private static final long APP_EXIT_INFO_FRESHNESS_MS = 300 * 1000; + + /** + * A mapping between uid -> pid -> {timestamp, extra info(Nullable)}. + * The uid here is the application uid, not the isolated uid. + */ + @GuardedBy("mLock") + private final SparseArray<SparseArray<Pair<Long, Object>>> mData; + + /** A tag for logging only */ + private final String mTag; + + /** A preset reason in case a proc dies */ + private final Integer mPresetReason; + + /** A callback that will be notified when a proc dies */ + private BiConsumer<Integer, Integer> mProcDiedListener; + + AppExitInfoExternalSource(String tag, Integer reason) { + mData = new SparseArray<SparseArray<Pair<Long, Object>>>(); + mTag = tag; + mPresetReason = reason; + } + + void add(int pid, int uid, Object extra) { + if (UserHandle.isIsolated(uid)) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + } + + synchronized (mLock) { + SparseArray<Pair<Long, Object>> array = mData.get(uid); + if (array == null) { + array = new SparseArray<Pair<Long, Object>>(); + mData.put(uid, array); + } + array.put(pid, new Pair<Long, Object>(System.currentTimeMillis(), extra)); + } + } + + Pair<Long, Object> remove(int pid, int uid) { + if (UserHandle.isIsolated(uid)) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + } + + synchronized (mLock) { + SparseArray<Pair<Long, Object>> array = mData.get(uid); + if (array != null) { + Pair<Long, Object> p = array.get(pid); + if (p != null) { + array.remove(pid); + return isFresh(p.first) ? p : null; + } + } + } + return null; + } + + void removeByUserId(int userId) { + if (userId == UserHandle.USER_CURRENT) { + userId = mService.mUserController.getCurrentUserId(); + } + synchronized (mLock) { + if (userId == UserHandle.USER_ALL) { + mData.clear(); + return; + } + for (int i = mData.size() - 1; i >= 0; i--) { + int uid = mData.keyAt(i); + if (UserHandle.getUserId(uid) == userId) { + mData.removeAt(i); + } + } + } + } + + void removeByUid(int uid, boolean allUsers) { + if (UserHandle.isIsolated(uid)) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + } + + if (allUsers) { + uid = UserHandle.getAppId(uid); + synchronized (mLock) { + for (int i = mData.size() - 1; i >= 0; i--) { + if (UserHandle.getAppId(mData.keyAt(i)) == uid) { + mData.removeAt(i); + } + } + } + } else { + synchronized (mLock) { + mData.remove(uid); + } + } + } + + void setOnProcDiedListener(BiConsumer<Integer, Integer> listener) { + synchronized (mLock) { + mProcDiedListener = listener; + } + } + + void onProcDied(final int pid, final int uid, final Integer status) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, mTag + ": proc died: pid=" + pid + " uid=" + uid + + ", status=" + status); + } + + if (mService == null) { + return; + } + + // Unlikely but possible: the record has been created + // Let's update it if we could find a ApplicationExitInfo record + if (!updateExitInfoIfNecessary(pid, uid, status, mPresetReason)) { + add(pid, uid, status); + } + + // Notify any interesed party regarding the lmkd kills + synchronized (mLock) { + final BiConsumer<Integer, Integer> listener = mProcDiedListener; + if (listener != null) { + mService.mHandler.post(()-> listener.accept(pid, uid)); + } + } + } + } +} diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index f86d6a70a076..0b115ea5ca5a 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -64,6 +64,7 @@ import static com.android.server.am.ActivityManagerService.TOP_APP_PRIORITY_BOOS import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; import android.app.ActivityManager; +import android.app.ApplicationExitInfo; import android.app.usage.UsageEvents; import android.content.Context; import android.content.pm.ServiceInfo; @@ -757,7 +758,10 @@ public final class OomAdjuster { lastCachedGroupUid = lastCachedGroup = 0; } if ((numCached - numCachedExtraGroup) > cachedProcessLimit) { - app.kill("cached #" + numCached, true); + app.kill("cached #" + numCached, + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_TOO_MANY_CACHED, + true); } break; case PROCESS_STATE_CACHED_EMPTY: @@ -765,11 +769,17 @@ public final class OomAdjuster { && app.lastActivityTime < oldTime) { app.kill("empty for " + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime) - / 1000) + "s", true); + / 1000) + "s", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_TRIM_EMPTY, + true); } else { numEmpty++; if (numEmpty > emptyProcessLimit) { - app.kill("empty #" + numEmpty, true); + app.kill("empty #" + numEmpty, + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_TOO_MANY_EMPTY, + true); } } break; @@ -786,7 +796,7 @@ public final class OomAdjuster { // definition not re-use the same process again, and it is // good to avoid having whatever code was running in them // left sitting around after no longer needed. - app.kill("isolated not needed", true); + app.kill("isolated not needed", ApplicationExitInfo.REASON_OTHER, true); } else { // Keeping this process, update its uid. updateAppUidRecLocked(app); @@ -2032,7 +2042,7 @@ public final class OomAdjuster { } if (app.waitingToKill != null && app.curReceivers.isEmpty() && app.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND) { - app.kill(app.waitingToKill, true); + app.kill(app.waitingToKill, ApplicationExitInfo.REASON_OTHER, true); success = false; } else { int processGroup; diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 2432e83c11b7..c8a8cd79c9b0 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -54,6 +54,9 @@ import android.app.ActivityManager; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppProtoEnums; +import android.app.ApplicationExitInfo; +import android.app.ApplicationExitInfo.Reason; +import android.app.ApplicationExitInfo.SubReason; import android.app.IApplicationThread; import android.content.ComponentName; import android.content.Context; @@ -424,12 +427,6 @@ public final class ProcessList { ActiveUids mActiveUids; /** - * The listener who is intereted with the lmkd kills. - */ - @GuardedBy("mService") - private LmkdKillListener mLmkdKillListener = null; - - /** * The currently running isolated processes. */ final SparseArray<ProcessRecord> mIsolatedProcesses = new SparseArray<>(); @@ -440,6 +437,12 @@ public final class ProcessList { final ProcessMap<AppZygote> mAppZygotes = new ProcessMap<AppZygote>(); /** + * Managees the {@link android.app.ApplicationExitInfo} records. + */ + @GuardedBy("mAppExitInfoTracker") + final AppExitInfoTracker mAppExitInfoTracker = new AppExitInfoTracker(); + + /** * The processes that are forked off an application zygote. */ final ArrayMap<AppZygote, ArrayList<ProcessRecord>> mAppZygoteProcesses = @@ -627,7 +630,6 @@ public final class ProcessList { final class KillHandler extends Handler { static final int KILL_PROCESS_GROUP_MSG = 4000; static final int LMKD_RECONNECT_MSG = 4001; - static final int LMKD_PROC_KILLED_MSG = 4002; public KillHandler(Looper looper) { super(looper, null, true); @@ -650,10 +652,6 @@ public final class ProcessList { KillHandler.LMKD_RECONNECT_MSG), LMKD_RECONNECT_DELAY_MS); } break; - case LMKD_PROC_KILLED_MSG: - handleLmkdProcKilled(msg.arg1 /* pid */, msg.arg2 /* uid */); - break; - default: super.handleMessage(msg); } @@ -722,9 +720,8 @@ public final class ProcessList { if (receivedLen != 12) { return false; } - sKillHandler.obtainMessage(KillHandler.LMKD_PROC_KILLED_MSG, - dataReceived.getInt(4), dataReceived.getInt(8)) - .sendToTarget(); + mAppExitInfoTracker.scheduleNoteLmkdProcKilled( + dataReceived.getInt(4), dataReceived.getInt(8)); return true; default: return false; @@ -739,9 +736,14 @@ public final class ProcessList { mSystemServerSocketForZygote.getFileDescriptor(), EVENT_INPUT, this::handleZygoteMessages); } + mAppExitInfoTracker.init(mService, sKillThread.getLooper()); } } + void onSystemReady() { + mAppExitInfoTracker.onSystemReady(); + } + void applyDisplaySize(WindowManagerService wm) { if (!mHaveDisplaySize) { Point p = new Point(); @@ -1474,7 +1476,10 @@ public final class ProcessList { proc.lastCachedPss, holder.appVersion); } } - proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", true); + proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_LARGE_CACHED, + true); } else if (proc != null && !keepIfLarge && mService.mLastMemoryLevel > ProcessStats.ADJ_MEM_FACTOR_NORMAL && proc.setProcState >= ActivityManager.PROCESS_STATE_CACHED_EMPTY) { @@ -1493,7 +1498,10 @@ public final class ProcessList { proc.lastCachedPss, holder.appVersion); } } - proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", true); + proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_LARGE_CACHED, + true); } } return proc; @@ -2188,6 +2196,9 @@ public final class ProcessList { if (doKill) { // do the killing ProcessList.killProcessGroup(app.uid, app.pid); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, + String.format(formatString, "")); } // wait for the death @@ -2266,6 +2277,8 @@ public final class ProcessList { app.pendingStart = false; killProcessQuiet(pid); Process.killProcessGroup(app.uid, app.pid); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, reason); return false; } mService.mBatteryStatsService.noteProcessStart(app.processName, app.info.uid); @@ -2351,6 +2364,8 @@ public final class ProcessList { if (app.pid > 0) { killProcessQuiet(app.pid); ProcessList.killProcessGroup(app.uid, app.pid); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, "hasn't been killed"); } else { app.pendingStart = false; } @@ -2481,6 +2496,12 @@ public final class ProcessList { @GuardedBy("mService") boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart, boolean allowRestart, String reason) { + return removeProcessLocked(app, callerWillRestart, allowRestart, reason, + ApplicationExitInfo.REASON_OTHER); + } + + boolean removeProcessLocked(ProcessRecord app, + boolean callerWillRestart, boolean allowRestart, String reason, int reasonCode) { final String name = app.processName; final int uid = app.uid; if (DEBUG_PROCESSES) Slog.d(TAG_PROCESSES, @@ -2516,7 +2537,7 @@ public final class ProcessList { needRestart = true; } } - app.kill(reason, true); + app.kill(reason, reasonCode, true); mService.handleAppDiedLocked(app, willRestart, allowRestart); if (willRestart) { removeLruProcessLocked(app); @@ -2601,6 +2622,7 @@ public final class ProcessList { // the uid of the isolated process is specified by the caller. uid = isolatedUid; } + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(uid, info.uid); mService.getPackageManagerInternalLocked().addIsolatedUid(uid, info.uid); // Register the isolated UID with this application so BatteryStats knows to @@ -3574,38 +3596,6 @@ public final class ProcessList { } } - void setLmkdKillListener(final LmkdKillListener listener) { - synchronized (mService) { - mLmkdKillListener = listener; - } - } - - private void handleLmkdProcKilled(final int pid, final int uid) { - // Log only now - if (DEBUG_PROCESSES) { - Slog.i(TAG, "lmkd kill: pid=" + pid + " uid=" + uid); - } - - if (mService == null) { - return; - } - // Notify any interesed party regarding the lmkd kills - synchronized (mService) { - final LmkdKillListener listener = mLmkdKillListener; - if (listener != null) { - mService.mHandler.post(()-> listener.onLmkdKillOccurred(pid, uid)); - } - } - } - - private void handleZygoteSigChld(int pid, int uid, int status) { - // Just log it now. - if (DEBUG_PROCESSES) { - Slog.i(TAG, "Got SIGCHLD from zygote: pid=" + pid + ", uid=" + uid - + ", status=" + Integer.toHexString(status)); - } - } - /** * Create a server socket in system_server, zygote will connect to it * in order to send unsolicited messages to system_server. @@ -3649,7 +3639,8 @@ public final class ProcessList { mZygoteUnsolicitedMessage.length); if (len > 0 && mZygoteSigChldMessage.length == Zygote.nativeParseSigChld( mZygoteUnsolicitedMessage, len, mZygoteSigChldMessage)) { - handleZygoteSigChld(mZygoteSigChldMessage[0] /* pid */, + mAppExitInfoTracker.handleZygoteSigChld( + mZygoteSigChldMessage[0] /* pid */, mZygoteSigChldMessage[1] /* uid */, mZygoteSigChldMessage[2] /* status */); } @@ -3659,4 +3650,39 @@ public final class ProcessList { } return EVENT_INPUT; } + + /** + * Called by ActivityManagerService when a process died. + */ + @GuardedBy("mService") + void noteProcessDiedLocked(final ProcessRecord app) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, "note: " + app + " died, saving the exit info"); + } + + mAppExitInfoTracker.scheduleNoteProcessDiedLocked(app); + } + + /** + * Called by ActivityManagerService when it decides to kill an application process. + */ + void noteAppKill(final ProcessRecord app, final @Reason int reason, + final @SubReason int subReason, final String msg) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, "note: " + app + " is being killed, reason: " + reason + + ", sub-reason: " + subReason + ", message: " + msg); + } + mAppExitInfoTracker.scheduleNoteAppKill(app, reason, subReason, msg); + } + + void noteAppKill(final int pid, final int uid, final @Reason int reason, + final @SubReason int subReason, final String msg) { + if (DEBUG_PROCESSES) { + Slog.i(TAG, "note: " + pid + " is being killed, reason: " + reason + + ", sub-reason: " + subReason + ", message: " + msg); + } + + mAppExitInfoTracker.scheduleNoteAppKill(pid, uid, reason, subReason, msg); + } } + diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 1fef3538165a..cbf058700909 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -27,6 +27,9 @@ import static com.android.server.am.ActivityManagerService.MY_PID; import android.app.ActivityManager; import android.app.ApplicationErrorReport; +import android.app.ApplicationExitInfo; +import android.app.ApplicationExitInfo.Reason; +import android.app.ApplicationExitInfo.SubReason; import android.app.Dialog; import android.app.IApplicationThread; import android.content.ComponentName; @@ -776,7 +779,8 @@ class ProcessRecord implements WindowProcessListener { } catch (RemoteException e) { // If it's already dead our work is done. If it's wedged just kill it. // We won't get the crash dialog or the error reporting. - kill("scheduleCrash for '" + message + "' failed", true); + kill("scheduleCrash for '" + message + "' failed", + ApplicationExitInfo.REASON_CRASH, true); } finally { Binder.restoreCallingIdentity(ident); } @@ -784,7 +788,11 @@ class ProcessRecord implements WindowProcessListener { } } - void kill(String reason, boolean noisy) { + void kill(String reason, @Reason int reasonCode, boolean noisy) { + kill(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy); + } + + void kill(String reason, @Reason int reasonCode, @SubReason int subReason, boolean noisy) { if (!killedByAm) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill"); if (mService != null && (noisy || info.uid == mService.mCurOomAdjUid)) { @@ -793,6 +801,7 @@ class ProcessRecord implements WindowProcessListener { info.uid); } if (pid > 0) { + mService.mProcessList.noteAppKill(this, reasonCode, subReason, reason); EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason); Process.killProcessQuiet(pid); ProcessList.killProcessGroup(uid, pid); @@ -1374,9 +1383,9 @@ class ProcessRecord implements WindowProcessListener { } @Override - public void appDied() { + public void appDied(String reason) { synchronized (mService) { - mService.appDiedLocked(this); + mService.appDiedLocked(this, reason); } } @@ -1441,7 +1450,8 @@ class ProcessRecord implements WindowProcessListener { ArrayList<Integer> firstPids = new ArrayList<>(5); SparseArray<Boolean> lastPids = new SparseArray<>(20); - mWindowProcessController.appEarlyNotResponding(annotation, () -> kill("anr", true)); + mWindowProcessController.appEarlyNotResponding(annotation, () -> kill("anr", + ApplicationExitInfo.REASON_ANR, true)); long anrTime = SystemClock.uptimeMillis(); if (isMonitorCpuUsage()) { @@ -1592,7 +1602,8 @@ class ProcessRecord implements WindowProcessListener { mService.addErrorToDropBox("anr", this, processName, activityShortComponentName, parentShortComponentName, parentPr, annotation, cpuInfo, tracesFile, null); - if (mWindowProcessController.appNotResponding(info.toString(), () -> kill("anr", true), + if (mWindowProcessController.appNotResponding(info.toString(), () -> kill("anr", + ApplicationExitInfo.REASON_ANR, true), () -> { synchronized (mService) { mService.mServices.scheduleServiceTimeoutLocked(this); @@ -1609,7 +1620,7 @@ class ProcessRecord implements WindowProcessListener { } if (isSilentAnr() && !isDebugging()) { - kill("bg anr", true); + kill("bg anr", ApplicationExitInfo.REASON_ANR, true); return; } diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 0a68408d49a5..8ef01e3f4776 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -946,7 +946,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { // This is the second time we failed -- finish activity and give up. Slog.e(TAG, "Second failure launching " + r.intent.getComponent().flattenToShortString() + ", giving up", e); - proc.appDied(); + proc.appDied("2nd-crash"); r.finishIfPossible("2nd-crash", false /* oomAdj */); return false; } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index ceb38f7d9789..c755e60ec977 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -971,11 +971,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio mAtm.mH.sendMessageAtFrontOfQueue(m); } - public void appDied() { + void appDied(String reason) { if (mListener == null) return; // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( - WindowProcessListener::appDied, mListener); + WindowProcessListener::appDied, mListener, reason); mAtm.mH.sendMessage(m); } @@ -1136,6 +1136,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } public void appEarlyNotResponding(String annotation, Runnable killAppCallback) { + Runnable targetRunnable = null; synchronized (mAtm.mGlobalLock) { if (mAtm.mController == null) { return; @@ -1145,13 +1146,16 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // 0 == continue, -1 = kill process immediately int res = mAtm.mController.appEarlyNotResponding(mName, mPid, annotation); if (res < 0 && mPid != MY_PID) { - killAppCallback.run(); + targetRunnable = killAppCallback; } } catch (RemoteException e) { mAtm.mController = null; Watchdog.getInstance().setActivityController(null); } } + if (targetRunnable != null) { + targetRunnable.run(); + } } public boolean appNotResponding(String info, Runnable killAppCallback, @@ -1179,6 +1183,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } if (targetRunnable != null) { + // Execute runnable outside WM lock since the runnable will hold AM lock targetRunnable.run(); return true; } diff --git a/services/core/java/com/android/server/wm/WindowProcessListener.java b/services/core/java/com/android/server/wm/WindowProcessListener.java index 870cbb00dff8..9505191d5eb3 100644 --- a/services/core/java/com/android/server/wm/WindowProcessListener.java +++ b/services/core/java/com/android/server/wm/WindowProcessListener.java @@ -59,7 +59,7 @@ public interface WindowProcessListener { long versionCode); /** App died :(...oh well */ - void appDied(); + void appDied(String reason); void dumpDebug(ProtoOutputStream proto, long fieldId); /** diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java new file mode 100644 index 000000000000..8d2a152dba83 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java @@ -0,0 +1,954 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; +import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; +import static android.app.ActivityManager.PROCESS_STATE_CACHED_EMPTY; +import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY; +import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.am.ActivityManagerService.Injector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.ApplicationExitInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Debug; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; +import android.system.OsConstants; +import android.text.TextUtils; +import android.util.Pair; + +import com.android.server.ServiceThread; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +/** + * Test class for {@link android.app.ApplicationExitInfo}. + * + * Build/Install/Run: + * atest ApplicationExitInfoTest + */ +@Presubmit +public class ApplicationExitInfoTest { + private static final String TAG = ApplicationExitInfoTest.class.getSimpleName(); + + @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); + @Mock private AppOpsService mAppOpsService; + @Mock private PackageManagerInternal mPackageManagerInt; + + private Context mContext = getInstrumentation().getTargetContext(); + private TestInjector mInjector; + private ActivityManagerService mAms; + private ProcessList mProcessList; + private AppExitInfoTracker mAppExitInfoTracker; + private Handler mHandler; + private HandlerThread mHandlerThread; + + @BeforeClass + public static void setUpOnce() { + System.setProperty("dexmaker.share_classloader", "true"); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + mProcessList = spy(new ProcessList()); + mAppExitInfoTracker = spy(new AppExitInfoTracker()); + setFieldValue(AppExitInfoTracker.class, mAppExitInfoTracker, "mIsolatedUidRecords", + spy(mAppExitInfoTracker.new IsolatedUidRecords())); + setFieldValue(AppExitInfoTracker.class, mAppExitInfoTracker, "mAppExitInfoSourceZygote", + spy(mAppExitInfoTracker.new AppExitInfoExternalSource("zygote", null))); + setFieldValue(AppExitInfoTracker.class, mAppExitInfoTracker, "mAppExitInfoSourceLmkd", + spy(mAppExitInfoTracker.new AppExitInfoExternalSource("lmkd", + ApplicationExitInfo.REASON_LOW_MEMORY))); + setFieldValue(ProcessList.class, mProcessList, "mAppExitInfoTracker", mAppExitInfoTracker); + mInjector = new TestInjector(mContext); + mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread()); + mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal()); + mAms.mPackageManagerInt = mPackageManagerInt; + } + + @After + public void tearDown() { + mHandlerThread.quit(); + } + + private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field mfield = Field.class.getDeclaredField("accessFlags"); + mfield.setAccessible(true); + mfield.setInt(field, mfield.getInt(field) & ~(Modifier.FINAL | Modifier.PRIVATE)); + field.set(obj, val); + } catch (NoSuchFieldException | IllegalAccessException e) { + } + } + + private void updateExitInfo(ProcessRecord app) { + ApplicationExitInfo raw = mAppExitInfoTracker.obtainRawRecordLocked(app); + mAppExitInfoTracker.handleNoteProcessDiedLocked(raw); + mAppExitInfoTracker.recycleRawRecordLocked(raw); + } + + private void noteAppKill(ProcessRecord app, int reason, int subReason, String msg) { + ApplicationExitInfo raw = mAppExitInfoTracker.obtainRawRecordLocked(app); + raw.setReason(reason); + raw.setSubReason(subReason); + raw.setDescription(msg); + mAppExitInfoTracker.handleNoteAppKillLocked(raw); + mAppExitInfoTracker.recycleRawRecordLocked(raw); + } + + @Test + public void testApplicationExitInfo() throws Exception { + mAppExitInfoTracker.clearProcessExitInfo(true); + mAppExitInfoTracker.mAppExitInfoLoaded = true; + + // Test application calls System.exit() + doNothing().when(mAppExitInfoTracker).schedulePersistProcessExitInfo(anyBoolean()); + + final int app1Uid = 10123; + final int app1Pid1 = 12345; + final int app1Pid2 = 12346; + final int app1DefiningUid = 23456; + final int app1ConnectiongGroup = 10; + final int app1UidUser2 = 1010123; + final int app1PidUser2 = 12347; + final int app1Pss1 = 34567; + final int app1Rss1 = 45678; + final int app1Pss2 = 34568; + final int app1Rss2 = 45679; + final int app1Pss3 = 34569; + final int app1Rss3 = 45680; + final String app1ProcessName = "com.android.test.stub1:process"; + final String app1PackageName = "com.android.test.stub1"; + + final long now1 = System.currentTimeMillis(); + ProcessRecord app = makeProcessRecord( + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_LAST_ACTIVITY, // procstate + app1Pss1, // pss + app1Rss1, // rss + app1ProcessName, // processName + app1PackageName); // packageName + + // Case 1: basic System.exit() test + int exitCode = 5; + doReturn(new Pair<Long, Object>(now1, Integer.valueOf(makeExitStatus(exitCode)))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + updateExitInfo(app); + + ArrayList<ApplicationExitInfo> list = new ArrayList<ApplicationExitInfo>(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + assertEquals(1, list.size()); + + ApplicationExitInfo info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now1, // timestamp + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_EXIT_SELF, // reason + null, // subReason + exitCode, // status + app1Pss1, // pss + app1Rss1, // rss + IMPORTANCE_CACHED, // importance + null); // description + + // Case 2: create another app1 process record with a different pid + sleep(1); + final long now2 = System.currentTimeMillis(); + app = makeProcessRecord( + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ConnectiongGroup, // connectionGroup + PROCESS_STATE_RECEIVER, // procstate + app1Pss2, // pss + app1Rss2, // rss + app1ProcessName, // processName + app1PackageName); // packageName + exitCode = 6; + doReturn(new Pair<Long, Object>(now2, Integer.valueOf(makeExitStatus(exitCode)))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + updateExitInfo(app); + list.clear(); + + // Get all the records for app1Uid + mAppExitInfoTracker.getExitInfo(app1PackageName, app1Uid, 0, 0, list); + assertEquals(2, list.size()); + + info = list.get(0); + + // Verify the most recent one + verifyApplicationExitInfo( + info, // info + now2, // timestamp + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + app1ConnectiongGroup, // connectionGroup + ApplicationExitInfo.REASON_EXIT_SELF, // reason + null, // subReason + exitCode, // status + app1Pss2, // pss + app1Rss2, // rss + IMPORTANCE_SERVICE, // importance + null); // description + + // Case 3: Create an instance of app1 with different user, and died because of SIGKILL + sleep(1); + final long now3 = System.currentTimeMillis(); + int sigNum = OsConstants.SIGKILL; + app = makeProcessRecord( + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_BOUND_FOREGROUND_SERVICE, // procstate + app1Pss3, // pss + app1Rss3, // rss + app1ProcessName, // processName + app1PackageName); // packageName + doReturn(new Pair<Long, Object>(now3, Integer.valueOf(makeSignalStatus(sigNum)))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + updateExitInfo(app); + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, app1PidUser2, 0, list); + + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now3, // timestamp + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_SIGNALED, // reason + null, // subReason + sigNum, // status + app1Pss3, // pss + app1Rss3, // rss + IMPORTANCE_FOREGROUND_SERVICE, // importance + null); // description + + // try go get all from app1UidUser2 + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, 0, 0, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now3, // timestamp + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_SIGNALED, // reason + null, // subReason + sigNum, // status + app1Pss3, // pss + app1Rss3, // rss + IMPORTANCE_FOREGROUND_SERVICE, // importance + null); // description + + // Case 4: Create a process from another package with kill from lmkd + final int app2UidUser2 = 1010234; + final int app2PidUser2 = 12348; + final int app2Pss1 = 54321; + final int app2Rss1 = 65432; + final String app2ProcessName = "com.android.test.stub2:process"; + final String app2PackageName = "com.android.test.stub2"; + + sleep(1); + final long now4 = System.currentTimeMillis(); + doReturn(new Pair<Long, Object>(now4, Integer.valueOf(0))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(new Pair<Long, Object>(now4, null)) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + + app = makeProcessRecord( + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_CACHED_EMPTY, // procstate + app2Pss1, // pss + app2Rss1, // rss + app2ProcessName, // processName + app2PackageName); // packageName + updateExitInfo(app); + list.clear(); + mAppExitInfoTracker.getExitInfo(app2PackageName, app2UidUser2, app2PidUser2, 0, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now4, // timestamp + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_LOW_MEMORY, // reason + null, // subReason + 0, // status + app2Pss1, // pss + app2Rss1, // rss + IMPORTANCE_CACHED, // importance + null); // description + + // Verify to get all from User2 regarding app2 + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app2UidUser2, 0, 0, list); + assertEquals(1, list.size()); + + // Case 5: App native crash + final int app3UidUser2 = 1010345; + final int app3PidUser2 = 12349; + final int app3ConnectiongGroup = 4; + final int app3Pss1 = 54320; + final int app3Rss1 = 65430; + final String app3ProcessName = "com.android.test.stub3:process"; + final String app3PackageName = "com.android.test.stub3"; + final String app3Description = "native crash"; + + sleep(1); + final long now5 = System.currentTimeMillis(); + sigNum = OsConstants.SIGABRT; + doReturn(new Pair<Long, Object>(now5, Integer.valueOf(makeSignalStatus(sigNum)))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + app = makeProcessRecord( + app3PidUser2, // pid + app3UidUser2, // uid + app3UidUser2, // packageUid + null, // definingUid + app3ConnectiongGroup, // connectionGroup + PROCESS_STATE_BOUND_TOP, // procstate + app3Pss1, // pss + app3Rss1, // rss + app3ProcessName, // processName + app3PackageName); // packageName + noteAppKill(app, ApplicationExitInfo.REASON_CRASH_NATIVE, + ApplicationExitInfo.SUBREASON_UNKNOWN, app3Description); + + updateExitInfo(app); + list.clear(); + mAppExitInfoTracker.getExitInfo(app3PackageName, app3UidUser2, app3PidUser2, 0, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now5, // timestamp + app3PidUser2, // pid + app3UidUser2, // uid + app3UidUser2, // packageUid + null, // definingUid + app3ProcessName, // processName + app3ConnectiongGroup, // connectionGroup + ApplicationExitInfo.REASON_CRASH_NATIVE, // reason + null, // subReason + sigNum, // status + app3Pss1, // pss + app3Rss1, // rss + IMPORTANCE_FOREGROUND, // importance + app3Description); // description + + // Verify the most recent kills, sorted by timestamp + int maxNum = 3; + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app3UidUser2, 0, maxNum, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now5, // timestamp + app3PidUser2, // pid + app3UidUser2, // uid + app3UidUser2, // packageUid + null, // definingUid + app3ProcessName, // processName + app3ConnectiongGroup, // connectionGroup + ApplicationExitInfo.REASON_CRASH_NATIVE, // reason + null, // subReason + sigNum, // status + app3Pss1, // pss + app3Rss1, // rss + IMPORTANCE_FOREGROUND, // importance + app3Description); // description + + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app2UidUser2, 0, maxNum, list); + assertEquals(1, list.size()); + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now4, // timestamp + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_LOW_MEMORY, // reason + null, // subReason + 0, // status + app2Pss1, // pss + app2Rss1, // rss + IMPORTANCE_CACHED, // importance + null); // description + + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app1UidUser2, 0, maxNum, list); + assertEquals(1, list.size()); + info = list.get(0); + + sigNum = OsConstants.SIGKILL; + verifyApplicationExitInfo( + info, // info + now3, // timestamp + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_SIGNALED, // reason + null, // subReason + sigNum, // status + app1Pss3, // pss + app1Rss3, // rss + IMPORTANCE_FOREGROUND_SERVICE, // importance + null); // description + + // Case 6: App Java crash + final int app3Uid = 10345; + final int app3IsolatedUid = 99001; // it's an isolated process + final int app3Pid = 12350; + final int app3Pss2 = 23232; + final int app3Rss2 = 32323; + final String app3Description2 = "force close"; + + sleep(1); + final long now6 = System.currentTimeMillis(); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + app = makeProcessRecord( + app3Pid, // pid + app3IsolatedUid, // uid + app3Uid, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_CACHED_EMPTY, // procstate + app3Pss2, // pss + app3Rss2, // rss + app3ProcessName, // processName + app3PackageName); // packageName + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app3IsolatedUid, app3Uid); + noteAppKill(app, ApplicationExitInfo.REASON_CRASH, + ApplicationExitInfo.SUBREASON_UNKNOWN, app3Description2); + + assertEquals(app3Uid, mAppExitInfoTracker.mIsolatedUidRecords + .getUidByIsolatedUid(app3IsolatedUid).longValue()); + updateExitInfo(app); + assertNull(mAppExitInfoTracker.mIsolatedUidRecords.getUidByIsolatedUid(app3IsolatedUid)); + + list.clear(); + mAppExitInfoTracker.getExitInfo(app3PackageName, app3Uid, 0, 1, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now6, // timestamp + app3Pid, // pid + app3IsolatedUid, // uid + app3Uid, // packageUid + null, // definingUid + app3ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_CRASH, // reason + null, // subReason + 0, // status + app3Pss2, // pss + app3Rss2, // rss + IMPORTANCE_CACHED, // importance + app3Description2); // description + + // Case 7: App1 is "uninstalled" from User2 + mAppExitInfoTracker.onPackageRemoved(app1PackageName, app1UidUser2, false); + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, 0, 0, list); + assertEquals(0, list.size()); + + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1Uid, 0, 0, list); + assertEquals(2, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now2, // timestamp + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + app1ConnectiongGroup, // connectionGroup + ApplicationExitInfo.REASON_EXIT_SELF, // reason + null, // subReason + exitCode, // status + app1Pss2, // pss + app1Rss2, // rss + IMPORTANCE_SERVICE, // importance + null); // description + + // Case 8: App1 gets "remove task" + final String app1Description = "remove task"; + + sleep(1); + final int app1IsolatedUidUser2 = 1099002; // isolated uid + final int app1Pss4 = 34343; + final int app1Rss4 = 43434; + final long now8 = System.currentTimeMillis(); + sigNum = OsConstants.SIGKILL; + doReturn(new Pair<Long, Object>(now8, makeSignalStatus(sigNum))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + app = makeProcessRecord( + app1PidUser2, // pid + app1IsolatedUidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_CACHED_EMPTY, // procstate + app1Pss4, // pss + app1Rss4, // rss + app1ProcessName, // processName + app1PackageName); // packageName + + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app1IsolatedUidUser2, app1UidUser2); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_UNKNOWN, app1Description); + + updateExitInfo(app); + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, app1PidUser2, 1, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now8, // timestamp + app1PidUser2, // pid + app1IsolatedUidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_OTHER, // reason + ApplicationExitInfo.SUBREASON_UNKNOWN, // subReason + 0, // status + app1Pss4, // pss + app1Rss4, // rss + IMPORTANCE_CACHED, // importance + app1Description); // description + + // App1 gets "too many empty" + final String app1Description2 = "too many empty"; + sleep(1); + final int app1Pid2User2 = 56565; + final int app1IsolatedUid2User2 = 1099003; // isolated uid + final int app1Pss5 = 34344; + final int app1Rss5 = 43435; + final long now9 = System.currentTimeMillis(); + sigNum = OsConstants.SIGKILL; + doReturn(new Pair<Long, Object>(now9, makeSignalStatus(sigNum))) + .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) + .remove(anyInt(), anyInt()); + doReturn(null) + .when(mAppExitInfoTracker.mAppExitInfoSourceLmkd) + .remove(anyInt(), anyInt()); + app = makeProcessRecord( + app1Pid2User2, // pid + app1IsolatedUid2User2, // uid + app1UidUser2, // packageUid + null, // definingUid + 0, // connectionGroup + PROCESS_STATE_CACHED_EMPTY, // procstate + app1Pss5, // pss + app1Rss5, // rss + app1ProcessName, // processName + app1PackageName); // packageName + + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app1IsolatedUid2User2, app1UidUser2); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_TOO_MANY_EMPTY, app1Description2); + + updateExitInfo(app); + list.clear(); + mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, app1Pid2User2, 1, list); + assertEquals(1, list.size()); + + info = list.get(0); + + verifyApplicationExitInfo( + info, // info + now9, // timestamp + app1Pid2User2, // pid + app1IsolatedUid2User2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_OTHER, // reason + ApplicationExitInfo.SUBREASON_TOO_MANY_EMPTY, // subReason + 0, // status + app1Pss5, // pss + app1Rss5, // rss + IMPORTANCE_CACHED, // importance + app1Description2); // description + + + // Case 9: User2 gets removed + sleep(1); + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app1IsolatedUidUser2, app1UidUser2); + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app3IsolatedUid, app3Uid); + + mAppExitInfoTracker.onUserRemoved(UserHandle.getUserId(app1UidUser2)); + + assertNull(mAppExitInfoTracker.mIsolatedUidRecords.getUidByIsolatedUid( + app1IsolatedUidUser2)); + assertNotNull(mAppExitInfoTracker.mIsolatedUidRecords.getUidByIsolatedUid( + app3IsolatedUid)); + mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid( + app1IsolatedUidUser2, app1UidUser2); + mAppExitInfoTracker.mIsolatedUidRecords.removeAppUid(app1UidUser2, false); + assertNull(mAppExitInfoTracker.mIsolatedUidRecords.getUidByIsolatedUid( + app1IsolatedUidUser2)); + mAppExitInfoTracker.mIsolatedUidRecords.removeAppUid(app3Uid, true); + assertNull(mAppExitInfoTracker.mIsolatedUidRecords.getUidByIsolatedUid(app3IsolatedUid)); + + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app1UidUser2, 0, 0, list); + assertEquals(0, list.size()); + + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app1Uid, 0, 0, list); + assertEquals(2, list.size()); + + info = list.get(0); + + exitCode = 6; + verifyApplicationExitInfo( + info, // info + now2, // timestamp + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + app1ConnectiongGroup, // connectionGroup + ApplicationExitInfo.REASON_EXIT_SELF, // reason + null, // subReason + exitCode, // status + app1Pss2, // pss + app1Rss2, // rss + IMPORTANCE_SERVICE, // importance + null); // description + + info = list.get(1); + exitCode = 5; + verifyApplicationExitInfo( + info, // info + now1, // timestamp + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + 0, // connectionGroup + ApplicationExitInfo.REASON_EXIT_SELF, // reason + null, // subReason + exitCode, // status + app1Pss1, // pss + app1Rss1, // rss + IMPORTANCE_CACHED, // importance + null); // description + + // Case 10: Save the info and load them again + ArrayList<ApplicationExitInfo> original = new ArrayList<ApplicationExitInfo>(); + mAppExitInfoTracker.getExitInfo(null, app1Uid, 0, 0, original); + assertTrue(original.size() > 0); + + mAppExitInfoTracker.mProcExitInfoFile = new File(mContext.getFilesDir(), + AppExitInfoTracker.APP_EXIT_INFO_FILE); + mAppExitInfoTracker.persistProcessExitInfo(); + assertTrue(mAppExitInfoTracker.mProcExitInfoFile.exists()); + + mAppExitInfoTracker.clearProcessExitInfo(false); + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app1Uid, 0, 0, list); + assertEquals(0, list.size()); + + mAppExitInfoTracker.loadExistingProcessExitInfo(); + list.clear(); + mAppExitInfoTracker.getExitInfo(null, app1Uid, 0, 0, list); + assertEquals(original.size(), list.size()); + + for (int i = list.size() - 1; i >= 0; i--) { + assertTrue(list.get(i).equals(original.get(i))); + } + } + + private static int makeExitStatus(int exitCode) { + return (exitCode << 8) & 0xff00; + } + + private static int makeSignalStatus(int sigNum) { + return sigNum & 0x7f; + } + + private void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + int connectionGroup, int procState, int pss, int rss, + String processName, String packageName) { + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid); + app.pid = pid; + app.info.uid = packageUid; + if (definingUid != null) { + final String dummyPackageName = "com.android.test"; + final String dummyClassName = ".Foo"; + app.hostingRecord = HostingRecord.byAppZygote(new ComponentName( + dummyPackageName, dummyClassName), "", definingUid); + } + app.connectionGroup = connectionGroup; + app.setProcState = procState; + app.lastMemInfo = spy(new Debug.MemoryInfo()); + doReturn(pss).when(app.lastMemInfo).getTotalPss(); + doReturn(rss).when(app.lastMemInfo).getTotalRss(); + return app; + } + + private void verifyApplicationExitInfo(ApplicationExitInfo info, + Long timestamp, Integer pid, Integer uid, Integer packageUid, + Integer definingUid, String processName, Integer connectionGroup, + Integer reason, Integer subReason, Integer status, + Integer pss, Integer rss, Integer importance, String description) { + assertNotNull(info); + + if (timestamp != null) { + final long tolerance = 1000; // ms + assertTrue(timestamp - tolerance <= info.getTimestamp()); + assertTrue(timestamp + tolerance >= info.getTimestamp()); + } + if (pid != null) { + assertEquals(pid.intValue(), info.getPid()); + } + if (uid != null) { + assertEquals(uid.intValue(), info.getRealUid()); + } + if (packageUid != null) { + assertEquals(packageUid.intValue(), info.getPackageUid()); + } + if (definingUid != null) { + assertEquals(definingUid.intValue(), info.getDefiningUid()); + } + if (processName != null) { + assertTrue(TextUtils.equals(processName, info.getProcessName())); + } + if (connectionGroup != null) { + assertEquals(connectionGroup.intValue(), info.getConnectionGroup()); + } + if (reason != null) { + assertEquals(reason.intValue(), info.getReason()); + } + if (subReason != null) { + assertEquals(subReason.intValue(), info.getSubReason()); + } + if (status != null) { + assertEquals(status.intValue(), info.getStatus()); + } + if (pss != null) { + assertEquals(pss.intValue(), info.getPss()); + } + if (rss != null) { + assertEquals(rss.intValue(), info.getRss()); + } + if (importance != null) { + assertEquals(importance.intValue(), info.getImportance()); + } + if (description != null) { + assertTrue(TextUtils.equals(description, info.getDescription())); + } + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File file, Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandler; + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mProcessList; + } + } + + static class ServiceThreadRule implements TestRule { + + private ServiceThread mThread; + + ServiceThread getThread() { + return mThread; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + mThread = new ServiceThread("TestServiceThread", + Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */); + mThread.start(); + try { + base.evaluate(); + } finally { + mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */); + } + } + }; + } + } +} |