Trigger an ANR for slow app responses.
When an app doesn't respond quickly enough to onStartJob or onStopJob,
JobScheduler stops tracking the job internally without giving any
indication to the app that the job is no longer considered operational.
Now, we trigger an ANR in these situations for apps targeting U+ so they
can get some signal (at least via analytics).
Bug: 258236856
Test: atest CtsJobSchedulerTestCases:JobSchedulingTest
Change-Id: I6a4266f779c553d0c60a75bd95280352531476f7
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index cc9e517..fec5281 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -24,7 +24,9 @@
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManagerInternal;
import android.app.Notification;
+import android.app.compat.CompatChanges;
import android.app.job.IJobCallback;
import android.app.job.IJobService;
import android.app.job.JobInfo;
@@ -32,6 +34,9 @@
import android.app.job.JobProtoEnums;
import android.app.job.JobWorkItem;
import android.app.usage.UsageStatsManagerInternal;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledAfter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -57,6 +62,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
+import com.android.internal.os.TimeoutRecord;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
@@ -87,6 +93,15 @@
private static final boolean DEBUG = JobSchedulerService.DEBUG;
private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY;
+ /**
+ * Whether to trigger an ANR when apps are slow to respond on pre-UDC APIs and functionality.
+ */
+ @ChangeId
+ @Disabled
+ // TODO(258236856): Enable after test is fixed
+ // @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ private static final long ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES = 258236856L;
+
private static final String TAG = "JobServiceContext";
/** Amount of time the JobScheduler waits for the initial service launch+bind. */
private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
@@ -119,6 +134,7 @@
/** Used for service binding, etc. */
private final Context mContext;
private final Object mLock;
+ private final ActivityManagerInternal mActivityManagerInternal;
private final IBatteryStats mBatteryStats;
private final EconomyManagerInternal mEconomyManagerInternal;
private final JobPackageTracker mJobPackageTracker;
@@ -270,6 +286,7 @@
mContext = service.getContext();
mLock = service.getLock();
mService = service;
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mBatteryStats = batteryStats;
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
mJobPackageTracker = tracker;
@@ -1121,23 +1138,31 @@
private void handleOpTimeoutLocked() {
switch (mVerb) {
case VERB_BINDING:
- Slog.w(TAG, "Time-out while trying to bind " + getRunningJobNameLocked()
- + ", dropping.");
- closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding");
+ onSlowAppResponseLocked(/* reschedule */ false, /* updateStopReasons */ true,
+ /* debugReason */ "timed out while binding",
+ /* anrMessage */ "Timed out while trying to bind",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_STARTING:
// Client unresponsive - wedged or failed to respond in time. We don't really
// know what happened so let's log it and notify the JobScheduler
// FINISHED/NO-RETRY.
- Slog.w(TAG, "No response from client for onStartJob "
- + getRunningJobNameLocked());
- closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting");
+ onSlowAppResponseLocked(/* reschedule */ false, /* updateStopReasons */ true,
+ /* debugReason */ "timed out while starting",
+ /* anrMessage */ "No response to onStartJob",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_STOPPING:
// At least we got somewhere, so fail but ask the JobScheduler to reschedule.
- Slog.w(TAG, "No response from client for onStopJob "
- + getRunningJobNameLocked());
- closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
+ // Don't update the stop reasons since we were already stopping the job for some
+ // other reason.
+ onSlowAppResponseLocked(/* reschedule */ true, /* updateStopReasons */ false,
+ /* debugReason */ "timed out while stopping",
+ /* anrMessage */ "No response to onStopJob",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_EXECUTING:
if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
@@ -1218,6 +1243,24 @@
}
}
+ @GuardedBy("mLock")
+ private void onSlowAppResponseLocked(boolean reschedule, boolean updateStopReasons,
+ @NonNull String debugReason, @NonNull String anrMessage, boolean triggerAnr) {
+ Slog.w(TAG, anrMessage + " for " + getRunningJobNameLocked());
+ if (updateStopReasons) {
+ mParams.setStopReason(
+ JobParameters.STOP_REASON_UNDEFINED,
+ JobParameters.INTERNAL_STOP_REASON_ANR,
+ debugReason);
+ }
+ if (triggerAnr) {
+ mActivityManagerInternal.appNotResponding(
+ mRunningJob.serviceProcessName, mRunningJob.getUid(),
+ TimeoutRecord.forJobService(anrMessage));
+ }
+ closeAndCleanupJobLocked(reschedule, debugReason);
+ }
+
/**
* The provided job has finished, either by calling
* {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 0fa1a37..324100f 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -504,6 +504,12 @@
public abstract void broadcastCloseSystemDialogs(String reason);
/**
+ * Trigger an ANR for the specified process.
+ */
+ public abstract void appNotResponding(@NonNull String processName, int uid,
+ @NonNull TimeoutRecord timeoutRecord);
+
+ /**
* Kills all background processes, except those matching any of the specified properties.
*
* @param minTargetSdk the target SDK version at or above which to preserve processes,
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index a587834..2f6091b 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -41,7 +41,9 @@
TimeoutKind.SERVICE_EXEC,
TimeoutKind.CONTENT_PROVIDER,
TimeoutKind.APP_REGISTERED,
- TimeoutKind.SHORT_FGS_TIMEOUT})
+ TimeoutKind.SHORT_FGS_TIMEOUT,
+ TimeoutKind.JOB_SERVICE,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface TimeoutKind {
@@ -53,6 +55,7 @@
int CONTENT_PROVIDER = 6;
int APP_REGISTERED = 7;
int SHORT_FGS_TIMEOUT = 8;
+ int JOB_SERVICE = 9;
}
/** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */
@@ -152,4 +155,10 @@
public static TimeoutRecord forShortFgsTimeout(String reason) {
return TimeoutRecord.endingNow(TimeoutKind.SHORT_FGS_TIMEOUT, reason);
}
+
+ /** Record for a job related timeout. */
+ @NonNull
+ public static TimeoutRecord forJobService(String reason) {
+ return TimeoutRecord.endingNow(TimeoutKind.JOB_SERVICE, reason);
+ }
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1fe097e..4f2f001 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -6770,6 +6770,21 @@
mAnrHelper.appNotResponding(anrProcess, timeoutRecord);
}
+ private void appNotResponding(@NonNull String processName, int uid,
+ @NonNull TimeoutRecord timeoutRecord) {
+ Objects.requireNonNull(processName);
+ Objects.requireNonNull(timeoutRecord);
+
+ synchronized (this) {
+ final ProcessRecord app = getProcessRecordLocked(processName, uid);
+ if (app == null) {
+ Slog.e(TAG, "Unknown process: " + processName);
+ return;
+ }
+ mAnrHelper.appNotResponding(app, timeoutRecord);
+ }
+ }
+
void startPersistentApps(int matchFlags) {
if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL) return;
@@ -17927,6 +17942,12 @@
}
@Override
+ public void appNotResponding(@NonNull String processName, int uid,
+ @NonNull TimeoutRecord timeoutRecord) {
+ ActivityManagerService.this.appNotResponding(processName, uid, timeoutRecord);
+ }
+
+ @Override
public void killAllBackgroundProcessesExcept(int minTargetSdk, int maxProcState) {
synchronized (ActivityManagerService.this) {
ActivityManagerService.this.killAllBackgroundProcessesExcept(