Merge "Trigger an ANR for slow app responses."
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 aa9212f..e654c38 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -509,6 +509,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 4d5baaf..9085e2a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -6811,6 +6811,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;
 
@@ -18016,6 +18031,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(