Report firstLaunch metrics

Track the stopped state and first launch state in the relevant
records, such as in BroadcastQueue and WindowProcessController
and reset at proper time. This fixes issues with the metrics for
stopped state.

Bug: 296644915
Test: atest ForceStopTest
      m statsd_testdrive; statsd_testdrive 48 475 476 477

Change-Id: Ic4508d770352ba61900f39d549b1fd82a2994c72
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 258f53d..5298846 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -119,6 +119,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SERVICE_EXECUTING;
 import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU;
@@ -1497,6 +1498,11 @@
         FrameworkStatsLog.write(FrameworkStatsLog.SERVICE_STATE_CHANGED, uid, packageName,
                 serviceName, FrameworkStatsLog.SERVICE_STATE_CHANGED__STATE__START);
         mAm.mBatteryStatsService.noteServiceStartRunning(uid, packageName, serviceName);
+        final ProcessRecord hostApp = r.app;
+        final boolean wasStopped = hostApp == null ? wasStopped(r) : false;
+        final boolean firstLaunch =
+                hostApp == null ? !mAm.wasPackageEverLaunched(r.packageName, r.userId) : false;
+
         String error = bringUpServiceLocked(r, service.getFlags(), callerFg,
                 false /* whileRestarting */,
                 false /* permissionsReviewRequired */,
@@ -1509,10 +1515,14 @@
             return new ComponentName("!!", error);
         }
 
-        final boolean wasStopped = (r.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
         final int packageState = wasStopped
                 ? SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
                 : SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
+        if (DEBUG_PROCESSES) {
+            Slog.d(TAG, "Logging startService for " + packageName + ", stopped="
+                    + wasStopped + ", firstLaunch=" + firstLaunch + ", intent=" + service
+                    + ", r.app=" + r.app);
+        }
         FrameworkStatsLog.write(SERVICE_REQUEST_EVENT_REPORTED, uid, callingUid,
                 service.getAction(),
                 SERVICE_REQUEST_EVENT_REPORTED__REQUEST_TYPE__START, false,
@@ -1527,7 +1537,9 @@
                 packageName,
                 callingPackage,
                 callingProcessState,
-                r.mProcessStateOnRequest);
+                r.mProcessStateOnRequest,
+                firstLaunch,
+                0L /* TODO: stoppedDuration */);
 
         if (r.startRequested && addToStarting) {
             boolean first = smap.mStartingBackground.size() == 0;
@@ -4038,7 +4050,6 @@
                 mAm.requireAllowedAssociationsLocked(s.appInfo.packageName);
             }
 
-            final boolean wasStopped = (s.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
             final boolean wasStartRequested = s.startRequested;
             final boolean hadConnections = !s.getConnections().isEmpty();
             mAm.startAssociationLocked(callerApp.uid, callerApp.processName,
@@ -4113,6 +4124,10 @@
                         true);
             }
 
+            final boolean wasStopped = hostApp == null ? wasStopped(s) : false;
+            final boolean firstLaunch =
+                    hostApp == null ? !mAm.wasPackageEverLaunched(s.packageName, s.userId) : false;
+
             boolean needOomAdj = false;
             if (c.hasFlag(Context.BIND_AUTO_CREATE)) {
                 s.lastActivity = SystemClock.uptimeMillis();
@@ -4155,6 +4170,10 @@
             final int packageState = wasStopped
                     ? SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
                     : SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
+            if (DEBUG_PROCESSES) {
+                Slog.d(TAG, "Logging bindService for " + s.packageName
+                        + ", stopped=" + wasStopped + ", firstLaunch=" + firstLaunch);
+            }
             FrameworkStatsLog.write(SERVICE_REQUEST_EVENT_REPORTED, s.appInfo.uid, callingUid,
                     ActivityManagerService.getShortAction(service.getAction()),
                     SERVICE_REQUEST_EVENT_REPORTED__REQUEST_TYPE__BIND, false,
@@ -4169,7 +4188,9 @@
                     s.packageName,
                     callerApp.info.packageName,
                     callerApp.mState.getCurProcState(),
-                    s.mProcessStateOnRequest);
+                    s.mProcessStateOnRequest,
+                    firstLaunch,
+                    0L /* TODO */);
 
             if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bind " + s + " with " + b
                     + ": received=" + b.intent.received
@@ -9112,4 +9133,8 @@
         return mCachedDeviceProvisioningPackage != null
                 && mCachedDeviceProvisioningPackage.equals(packageName);
     }
+
+    private boolean wasStopped(ServiceRecord serviceRecord) {
+        return (serviceRecord.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
+    }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5e36709..7bd6745 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5046,8 +5046,11 @@
      * Send LOCKED_BOOT_COMPLETED and BOOT_COMPLETED to the package explicitly when unstopped
      */
     private void maybeSendBootCompletedLocked(ProcessRecord app) {
+        if (!android.content.pm.Flags.stayStopped()) return;
         // Nothing to do if it wasn't previously stopped
-        if (!android.content.pm.Flags.stayStopped() || !app.wasForceStopped()) return;
+        if (!app.wasForceStopped() && !app.getWindowProcessController().wasForceStopped()) {
+            return;
+        }
 
         // Send LOCKED_BOOT_COMPLETED, if necessary
         if (app.getApplicationInfo().isEncryptionAware()) {
@@ -5059,7 +5062,8 @@
             sendBootBroadcastToAppLocked(app, new Intent(Intent.ACTION_BOOT_COMPLETED),
                     REASON_BOOT_COMPLETED);
         }
-        app.setWasForceStopped(false);
+        // The stopped state is reset in ProcessRecord when the pid changes, to deal with
+        // any re-use of the ProcessRecord.
     }
 
     /** Send a boot_completed broadcast to app */
@@ -6844,6 +6848,17 @@
         return mPermissionManagerInt;
     }
 
+    /** Returns whether the given package was ever launched since install */
+    boolean wasPackageEverLaunched(String packageName, @UserIdInt int userId) {
+        boolean wasLaunched = false;
+        try {
+            wasLaunched = getPackageManagerInternal().wasPackageEverLaunched(packageName, userId);
+        } catch (Exception e) {
+            // If the package state record doesn't exist yet, assume it was never launched
+        }
+        return wasLaunched;
+    }
+
     private TestUtilityService getTestUtilityServiceLocked() {
         if (mTestUtilityService == null) {
             mTestUtilityService =
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 1dc384d..3e633cc 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -54,6 +54,7 @@
 import com.android.server.IoThread;
 import com.android.server.ServiceThread;
 import com.android.server.SystemServiceManager;
+import com.android.server.wm.WindowProcessController;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -385,8 +386,10 @@
         start.setPackageName(app.info.packageName);
         if (android.content.pm.Flags.stayStopped()) {
             // TODO: Verify this is created at the right time to have the correct force-stopped
-            // state in the ProcessRecord. Also use the WindowProcessRecord if activity.
-            start.setForceStopped(app.wasForceStopped());
+            // state in the ProcessRecord.
+            final WindowProcessController wpc = app.getWindowProcessController();
+            start.setForceStopped(app.wasForceStopped()
+                    || (wpc != null ? wpc.wasForceStopped() : false));
         }
     }
 
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 4a37913..b339631 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -181,6 +181,12 @@
     private boolean mActiveWasStopped;
 
     /**
+     * Flag indicating that the currently active broadcast is being dispatched
+     * to a package that was never launched before.
+     */
+    private boolean mActiveFirstLaunch;
+
+    /**
      * Number of consecutive urgent broadcasts that have been dispatched
      * since the last non-urgent dispatch.
      */
@@ -596,6 +602,10 @@
         mActiveWasStopped = activeWasStopped;
     }
 
+    public void setActiveFirstLaunch(boolean activeFirstLaunch) {
+        mActiveFirstLaunch = activeFirstLaunch;
+    }
+
     public boolean getActiveViaColdStart() {
         return mActiveViaColdStart;
     }
@@ -604,6 +614,10 @@
         return mActiveWasStopped;
     }
 
+    public boolean getActiveFirstLaunch() {
+        return mActiveFirstLaunch;
+    }
+
     /**
      * Get package name of the first application loaded into this process.
      */
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 4422608..98a1ea5 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -32,6 +32,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
 import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.reasonToString;
@@ -966,6 +967,9 @@
             queue.setActiveWasStopped(true);
         }
         final int intentFlags = r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND;
+        final boolean firstLaunch = !mService.wasPackageEverLaunched(info.packageName, r.userId);
+        queue.setActiveFirstLaunch(firstLaunch);
+
         final HostingRecord hostingRecord = new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST,
                 component, r.intent.getAction(), r.getHostingRecordTriggerType());
         final boolean isActivityCapable = (r.options != null
@@ -2108,6 +2112,12 @@
         final long dispatchDelay = r.scheduledTime[index] - r.enqueueTime;
         final long receiveDelay = 0;
         final long finishDelay = r.terminalTime[index] - r.scheduledTime[index];
+        if (DEBUG_PROCESSES) {
+            Slog.d(TAG, "Logging broadcast for "
+                    + (app != null ? app.info.packageName : "<null>")
+                    + ", stopped=" + queue.getActiveWasStopped()
+                    + ", firstLaunch=" + queue.getActiveFirstLaunch());
+        }
         if (queue != null) {
             final int packageState = queue.getActiveWasStopped()
                     ? SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
@@ -2117,7 +2127,11 @@
                     app != null ? app.info.packageName : null, r.callerPackage,
                     r.calculateTypeForLogging(), r.getDeliveryGroupPolicy(), r.intent.getFlags(),
                     BroadcastRecord.getReceiverPriority(receiver), r.callerProcState,
-                    receiverProcessState);
+                    receiverProcessState, queue.getActiveFirstLaunch(),
+                    0L /* TODO: stoppedDuration */);
+            // Reset the states after logging
+            queue.setActiveFirstLaunch(false);
+            queue.setActiveWasStopped(false);
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index cb7898d..f76bf37 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -34,6 +34,7 @@
 import static com.android.internal.util.FrameworkStatsLog.PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
 import static com.android.internal.util.FrameworkStatsLog.PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
 import static com.android.server.am.ActivityManagerService.TAG_MU;
 import static com.android.server.am.Flags.serviceBindingOomAdjPolicy;
 
@@ -290,7 +291,8 @@
                             PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM,
                             PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL,
                             cpi.packageName, callingPackage,
-                            callingProcessState, callingProcessState);
+                            callingProcessState, callingProcessState,
+                            false, 0L);
                     return holder;
                 }
 
@@ -368,7 +370,7 @@
                                 PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM,
                                 PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL,
                                 cpi.packageName, callingPackage,
-                                callingProcessState, providerProcessState);
+                                callingProcessState, providerProcessState, false, 0L);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(origId);
@@ -546,12 +548,16 @@
                                     PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM,
                                     PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL,
                                     cpi.packageName, callingPackage,
-                                    callingProcessState, proc.mState.getCurProcState());
+                                    callingProcessState, proc.mState.getCurProcState(),
+                                    false, 0L);
                         } else {
-                            final int packageState =
-                                    ((cpr.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0)
+                            final boolean stopped =
+                                    (cpr.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
+                            final int packageState = stopped
                                     ? PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
                                     : PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
+                            final boolean firstLaunch = !mService.wasPackageEverLaunched(
+                                    cpi.packageName, userId);
                             checkTime(startTime, "getContentProviderImpl: before start process");
                             proc = mService.startProcessLocked(
                                     cpi.processName, cpr.appInfo, false, 0,
@@ -567,12 +573,18 @@
                                         + ": process is bad");
                                 return null;
                             }
+                            if (DEBUG_PROCESSES) {
+                                Slog.d(TAG, "Logging provider access for " + cpi.packageName
+                                        + ", stopped=" + stopped + ", firstLaunch=" + firstLaunch);
+                            }
                             FrameworkStatsLog.write(
                                     PROVIDER_ACQUISITION_EVENT_REPORTED,
                                     proc.uid, callingUid,
                                     PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD,
                                     packageState, cpi.packageName, callingPackage,
-                                    callingProcessState, ActivityManager.PROCESS_STATE_NONEXISTENT);
+                                    callingProcessState, ActivityManager.PROCESS_STATE_NONEXISTENT,
+                                    firstLaunch,
+                                    0L /* TODO: stoppedDuration */);
                         }
                         cpr.launchingApp = proc;
                         mLaunchingProviders.add(cpr);
diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java
index 30811a1..1a78a13 100644
--- a/services/core/java/com/android/server/am/HostingRecord.java
+++ b/services/core/java/com/android/server/am/HostingRecord.java
@@ -325,4 +325,15 @@
                 return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
         }
     }
+
+    private static boolean isTypeActivity(String hostingType) {
+        return HOSTING_TYPE_ACTIVITY.equals(hostingType)
+                || HOSTING_TYPE_NEXT_ACTIVITY.equals(hostingType)
+                || HOSTING_TYPE_NEXT_TOP_ACTIVITY.equals(hostingType)
+                || HOSTING_TYPE_TOP_ACTIVITY.equals(hostingType);
+    }
+
+    public boolean isTypeActivity() {
+        return isTypeActivity(mHostingType);
+    }
 }
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index a1fdd50..27d6c60 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -59,6 +59,8 @@
 import static com.android.server.am.ActivityManagerService.TAG_NETWORK;
 import static com.android.server.am.ActivityManagerService.TAG_PROCESSES;
 import static com.android.server.am.ActivityManagerService.TAG_UID_OBSERVERS;
+import static com.android.server.wm.WindowProcessController.STOPPED_STATE_FIRST_LAUNCH;
+import static com.android.server.wm.WindowProcessController.STOPPED_STATE_FORCE_STOPPED;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -3327,19 +3329,24 @@
                 hostingRecord.getDefiningUid(), hostingRecord.getDefiningProcessName());
         final ProcessStateRecord state = r.mState;
 
+        final boolean wasStopped = (info.flags & ApplicationInfo.FLAG_STOPPED) != 0;
         // Check if we should mark the processrecord for first launch after force-stopping
-        if ((r.getApplicationInfo().flags & ApplicationInfo.FLAG_STOPPED) != 0) {
-            try {
-                final boolean wasPackageEverLaunched = mService.getPackageManagerInternal()
+        if (wasStopped) {
+            // Check if the hosting record is for an activity or not. Since the stopped
+            // state tracking is handled differently to avoid WM calling back into AM,
+            // store the state in the correct record
+            if (hostingRecord.isTypeActivity()) {
+                final boolean wasPackageEverLaunched = mService
                         .wasPackageEverLaunched(r.getApplicationInfo().packageName, r.userId);
-                // If the package was launched in the past but is currently stopped, only then it
-                // should be considered as stopped after use. Do not mark it if it's the
-                // first launch.
-                if (wasPackageEverLaunched) {
-                    r.setWasForceStopped(true);
-                }
-            } catch (IllegalArgumentException e) {
-                // App doesn't have state yet, so wasn't forcestopped
+                // If the package was launched in the past but is currently stopped, only then
+                // should it be considered as force-stopped.
+                @WindowProcessController.StoppedState int stoppedState = wasPackageEverLaunched
+                        ? STOPPED_STATE_FORCE_STOPPED
+                        : STOPPED_STATE_FIRST_LAUNCH;
+                r.getWindowProcessController().setStoppedState(stoppedState);
+            } else {
+                r.setWasForceStopped(true);
+                // first launch is computed just before logging, for non-activity types
             }
         }
 
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 7356588..9fa3a8b 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -439,6 +439,7 @@
     final ProcessRecordNode[] mLinkedNodes = new ProcessRecordNode[NUM_NODE_TYPE];
 
     /** Whether the app was launched from a stopped state and is being unstopped. */
+    @GuardedBy("mService")
     volatile boolean mWasForceStopped;
 
     void setStartParams(int startUid, HostingRecord hostingRecord, String seInfo,
@@ -684,6 +685,11 @@
 
     @GuardedBy({"mService", "mProcLock"})
     void setPid(int pid) {
+        // If the pid is changing and not the first time pid is being assigned, clear stopped state
+        // So if the process record is re-used for a different pid, it wouldn't keep the state.
+        if (pid != mPid && mPid != 0) {
+            setWasForceStopped(false);
+        }
         mPid = pid;
         mWindowProcessController.setPid(pid);
         mShortStringName = null;
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index feab2c05..bac5132 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -8,6 +8,7 @@
         { "include-filter": "android.app.cts.ActivityManagerProcessStateTest" },
         { "include-filter": "android.app.cts.ServiceTest" },
         { "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" },
+        { "include-filter": "android.app.cts.ForceStopTest" },
         {
           "include-annotation": "android.platform.test.annotations.Presubmit"
         },
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 78f501a..59a56de 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -1133,10 +1133,12 @@
             isIncremental = true;
             isLoading = isIncrementalLoading(info.packageName, info.userId);
         }
-        final boolean stopped = (info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
+        final boolean stopped = wasStoppedNeedsLogging(info);
         final int packageState = stopped
                 ? APP_START_OCCURRED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
                 : APP_START_OCCURRED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
+
+        final boolean firstLaunch = wasFirstLaunch(info);
         FrameworkStatsLog.write(
                 FrameworkStatsLog.APP_START_OCCURRED,
                 info.applicationInfo.uid,
@@ -1163,18 +1165,26 @@
                 TimeUnit.NANOSECONDS.toMillis(info.timestampNs),
                 processState,
                 processOomAdj,
-                packageState);
+                packageState,
+                false, // is_xr_activity
+                firstLaunch,
+                0L /* TODO: stoppedDuration */);
+        // Reset the stopped state to avoid reporting stopped again
+        if (info.processRecord != null) {
+            info.processRecord.setWasStoppedLogged(true);
+        }
 
         if (DEBUG_METRICS) {
-            Slog.i(TAG, String.format("APP_START_OCCURRED(%s, %s, %s, %s, %s)",
+            Slog.i(TAG, String.format(
+                    "APP_START_OCCURRED(%s, %s, %s, %s, %s, wasStopped=%b, firstLaunch=%b)",
                     info.applicationInfo.uid,
                     info.packageName,
                     getAppStartTransitionType(info.type, info.relaunched),
                     info.launchedActivityName,
-                    info.launchedActivityLaunchedFromPackage));
+                    info.launchedActivityLaunchedFromPackage,
+                    stopped, firstLaunch));
         }
 
-
         logAppStartMemoryStateCapture(info);
     }
 
@@ -1794,4 +1804,28 @@
                 return -1;
         }
     }
+
+    private boolean wasStoppedNeedsLogging(TransitionInfoSnapshot info) {
+        if (info.processRecord != null) {
+            return (info.processRecord.wasForceStopped()
+                        || info.processRecord.wasFirstLaunch())
+                    && !info.processRecord.getWasStoppedLogged();
+        } else {
+            return (info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
+        }
+    }
+
+    private boolean wasFirstLaunch(TransitionInfoSnapshot info) {
+        if (info.processRecord != null) {
+            return info.processRecord.wasFirstLaunch()
+                    && !info.processRecord.getWasStoppedLogged();
+        }
+        try {
+            return !mSupervisor.mService.getPackageManagerInternalLocked()
+                    .wasPackageEverLaunched(info.packageName, info.userId);
+        } catch (Exception e) {
+            // Couldn't find the state record, so must be a newly installed app
+            return true;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index ee16a37..6ac2774 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -114,6 +114,10 @@
     private static final long RAPID_ACTIVITY_LAUNCH_MS = 300;
     private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 5 * RAPID_ACTIVITY_LAUNCH_MS;
 
+    public static final int STOPPED_STATE_NOT_STOPPED = 0;
+    public static final int STOPPED_STATE_FIRST_LAUNCH = 1;
+    public static final int STOPPED_STATE_FORCE_STOPPED = 2;
+
     private int mRapidActivityLaunchCount;
 
     // all about the first app in the process
@@ -281,6 +285,22 @@
     @AnimatingReason
     private int mAnimatingReasons;
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STOPPED_STATE_NOT_STOPPED,
+            STOPPED_STATE_FIRST_LAUNCH,
+            STOPPED_STATE_FORCE_STOPPED
+    })
+    public @interface StoppedState {}
+
+    private volatile @StoppedState int mStoppedState;
+
+    /**
+     * Whether the stopped state was logged for an activity start, as we don't want to log
+     * multiple times.
+     */
+    private volatile boolean mWasStoppedLogged;
+
     // The bits used for mActivityStateFlags.
     private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16;
     private static final int ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED = 1 << 17;
@@ -1928,6 +1948,29 @@
                 && (mInfo.flags & ApplicationInfo.FLAG_FACTORY_TEST) != 0;
     }
 
+    /** Sets the current stopped state of the app, which is reset as soon as metrics are logged */
+    public void setStoppedState(@StoppedState int stoppedState) {
+        mStoppedState = stoppedState;
+    }
+
+    boolean getWasStoppedLogged() {
+        return mWasStoppedLogged;
+    }
+
+    void setWasStoppedLogged(boolean logged) {
+        mWasStoppedLogged = logged;
+    }
+
+    /** Returns whether the app had been force-stopped before this launch */
+    public boolean wasForceStopped() {
+        return mStoppedState == STOPPED_STATE_FORCE_STOPPED;
+    }
+
+    /** Returns whether this app is being launched for the first time since install */
+    boolean wasFirstLaunch() {
+        return mStoppedState == STOPPED_STATE_FIRST_LAUNCH;
+    }
+
     void setRunningRecentsAnimation(boolean running) {
         if (running) {
             addAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index bcf297f..fff08c4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -42,6 +42,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -1475,7 +1476,8 @@
                 eq(BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST),
                 eq(BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD),
                 anyLong(), anyLong(), anyLong(), anyInt(), nullable(String.class),
-                anyString(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt()),
+                anyString(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(),
+                anyBoolean(), anyLong()),
                 times(1));
     }