Snap for 12582784 from a5aa2cde40a6c66c9751517361c3a966c9cf06fa to mainline-adservices-release

Change-Id: I3ba080c26186a1616937a1bc7793aba1e062e426
diff --git a/framework/java/android/os/flags.aconfig b/framework/java/android/os/flags.aconfig
index 7ccdaa7..1b21e88 100644
--- a/framework/java/android/os/flags.aconfig
+++ b/framework/java/android/os/flags.aconfig
@@ -27,12 +27,13 @@
      is_exported: true
      description: "Enables profiling queue persist and restore."
      bug: "342435438"
-     }
+}
 
 flag {
-     name: "system_triggered_profiling"
+     name: "system_triggered_profiling_new"
      namespace: "system_performance"
      is_exported: true
      description: "Enables system triggered profiling apis and functionality."
+     is_fixed_read_only: true
      bug: "373461116"
 }
diff --git a/service/java/com/android/os/profiling/Configs.java b/service/java/com/android/os/profiling/Configs.java
index 7b8be88..27225ca 100644
--- a/service/java/com/android/os/profiling/Configs.java
+++ b/service/java/com/android/os/profiling/Configs.java
@@ -41,6 +41,8 @@
 
     private static final int FOUR_MB = 4096;
 
+    private static final int ONE_DAY_MS = 24 * 60 * 60 * 1000;
+
     private static boolean sSystemTriggeredSystemTraceConfigsInitialized = false;
     private static boolean sSystemTraceConfigsInitialized = false;
     private static boolean sHeapProfileConfigsInitialized = false;
@@ -771,9 +773,13 @@
         return builder.build().toByteArray();
     }
 
-    /** Generate config for system triggered background system trace. */
+    /**
+     * Generate config for system triggered background system trace.
+     *
+     * @param extraLong should only be set to true for testing.
+     */
     public static byte[] generateSystemTriggeredTraceConfig(String uniqueSessionName,
-            String[] packageNames) {
+            String[] packageNames, boolean extraLong) {
         // Make sure we have our config values set. This is the only config specific method which is
         // called directly and therefore needs to verify the config value initialization directly.
         initializeSystemTriggeredSystemTraceConfigsIfNecessary();
@@ -785,7 +791,9 @@
                 packageNames,
                 sSystemTriggeredSystemTraceDiscardBufferSizeKb,
                 sSystemTriggeredSystemTraceRingBufferSizeKb,
-                sSystemTriggeredSystemTraceDurationMs,
+                extraLong
+                        ? ONE_DAY_MS
+                        : sSystemTriggeredSystemTraceDurationMs,
                 TraceConfig.BufferConfig.FillPolicy.RING_BUFFER);
 
         builder.setUniqueSessionName(uniqueSessionName);
diff --git a/service/java/com/android/os/profiling/DeviceConfigHelper.java b/service/java/com/android/os/profiling/DeviceConfigHelper.java
index 72d1621..f29de40 100644
--- a/service/java/com/android/os/profiling/DeviceConfigHelper.java
+++ b/service/java/com/android/os/profiling/DeviceConfigHelper.java
@@ -33,6 +33,8 @@
     public static final String RATE_LIMITER_DISABLE_PROPERTY = "rate_limiter.disabled";
     public static final String DISABLE_DELETE_UNREDACTED_TRACE =
             "delete_unredacted_trace.disabled";
+    public static final String SYSTEM_TRIGGERED_TEST_PACKAGE_NAME =
+            "system_triggered_profiling.testing_package_name";
 
     // End section: Testing specific constants
 
@@ -130,8 +132,6 @@
             "clear_temporary_directory_frequency_ms";
     public static final String CLEAR_TEMPORARY_DIRECTORY_BOOT_DELAY_MS =
             "clear_temporary_directory_boot_delay_ms";
-    public static final String PERSIST_QUEUE_TO_DISK_FREQUENCY_MS =
-            "persist_queue_to_disk_frequency_ms";
 
     // Post Processing Configs
     public static final String PROFILING_RECHECK_DELAY_MS = "profiling_recheck_delay_ms";
diff --git a/service/java/com/android/os/profiling/ProfilingService.java b/service/java/com/android/os/profiling/ProfilingService.java
index 2342b5a..33e602e 100644
--- a/service/java/com/android/os/profiling/ProfilingService.java
+++ b/service/java/com/android/os/profiling/ProfilingService.java
@@ -34,6 +34,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.ProfilingManager;
 import android.os.ProfilingResult;
+import android.os.ProfilingTriggersWrapper;
 import android.os.QueuedResultsWrapper;
 import android.os.RemoteException;
 import android.provider.DeviceConfig;
@@ -62,6 +63,7 @@
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 
 public class ProfilingService extends IProfilingService.Stub {
     private static final String TAG = ProfilingService.class.getSimpleName();
@@ -80,9 +82,10 @@
     private static final String OUTPUT_FILE_TRIGGER = "trigger";
     private static final String OUTPUT_FILE_IN_PROGRESS = "in-progress";
 
-    private static final String QUEUED_RESULTS_SYSTEM_DIR = "system";
-    private static final String QUEUED_RESULTS_STORE_DIR = "profiling_queued_results_store";
+    private static final String PERSIST_SYSTEM_DIR = "system";
+    private static final String PERSIST_STORE_DIR = "profiling_service_data";
     private static final String QUEUED_RESULTS_INFO_FILE = "profiling_queued_results_info";
+    private static final String APP_TRIGGERS_INFO_FILE = "profiling_app_triggers_info";
 
     // Used for unique session name only, not filename.
     private static final String SYSTEM_TRIGGERED_SESSION_NAME_PREFIX = "system_triggered_session_";
@@ -109,7 +112,7 @@
     @VisibleForTesting
     public static final int QUEUED_RESULT_MAX_RETAINED_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
 
-    private static final int PERSIST_QUEUE_TO_DISK_FREQUENCY_MS = 30 * 60 * 1000;
+    private static final int PERSIST_TO_DISK_DEFAULT_FREQUENCY_MS = 30 * 60 * 1000;
 
     private final Context mContext;
     private final Object mLock = new Object();
@@ -156,36 +159,54 @@
     // Map of uid + package name to a sparse array of trigger objects.
     @VisibleForTesting
     public ProcessMap<SparseArray<ProfilingTrigger>> mAppTriggers = new ProcessMap<>();
+    @VisibleForTesting
+    public boolean mAppTriggersLoaded = false;
 
     // uid indexed storage of completed tracing sessions that have not yet successfully handled the
     // result.
     @VisibleForTesting
     public SparseArray<List<TracingSession>> mQueuedTracingResults = new SparseArray<>();
 
-    private boolean mPersistQueueScheduled = false;
+    private boolean mPersistScheduled = false;
     // Frequency of 0 would result in immediate persist.
     @GuardedBy("mLock")
-    private AtomicInteger mPersistQueueFrequencyMs;
+    private AtomicInteger mPersistFrequencyMs;
     @GuardedBy("mLock")
-    private long mLastPersistedQueueTimestampMs = 0L;
-    private Runnable mPersistQueueRunnable = null;
+    private long mLastPersistedTimestampMs = 0L;
+    private Runnable mPersistRunnable = null;
 
-    /**
-     * The path to the directory which includes the queued results data file as specified in
-     * {@link #mPersistQueueFile}.
-     */
+    /** The path to the directory which includes all persisted results from this class. */
     @VisibleForTesting
-    public File mPersistQueueStoreDir;
+    public File mPersistStoreDir = null;
 
     /** The queued results data file, persisted in the storage. */
     @VisibleForTesting
-    public File mPersistQueueFile;
+    public File mPersistQueueFile = null;
+
+    /** The app triggers results data file, persisted in the storage. */
+    @VisibleForTesting
+    public File mPersistAppTriggersFile = null;
 
     /** To be disabled for testing only. */
     @GuardedBy("mLock")
     private boolean mKeepUnredactedTrace = false;
 
     /**
+     * Package name of app being tested, or null if no app is being tested. To be used both for
+     * automated testing and developer manual testing.
+     *
+     * Setting this package name will:
+     * - Ensure a system triggered trace is always running.
+     * - Allow all triggers for the specified package name to be executed.
+     *
+     * This is not intended to be set directly. Instead, set this package name by using
+     * device_config commands described at {@link ProfilingManager}.
+     *
+     * There is no time limit on how long this can be left enabled for.
+     */
+    private String mTestPackageName = null;
+
+    /**
      * State the {@link TracingSession} is in.
      *
      * State represents the most recently confirmed completed step in the process. Steps represent
@@ -285,9 +306,9 @@
             mKeepUnredactedTrace = DeviceConfigHelper.getTestBoolean(
                     DeviceConfigHelper.DISABLE_DELETE_UNREDACTED_TRACE, false);
 
-            mPersistQueueFrequencyMs = new AtomicInteger(DeviceConfigHelper.getInt(
-                    DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS,
-                    PERSIST_QUEUE_TO_DISK_FREQUENCY_MS));
+            mPersistFrequencyMs = new AtomicInteger(DeviceConfigHelper.getInt(
+                    DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS,
+                    PERSIST_TO_DISK_DEFAULT_FREQUENCY_MS));
         }
         // Now subscribe to updates on test config.
         DeviceConfig.addOnPropertiesChangedListener(DeviceConfigHelper.NAMESPACE_TESTING,
@@ -298,6 +319,10 @@
                             mKeepUnredactedTrace = properties.getBoolean(
                                     DeviceConfigHelper.DISABLE_DELETE_UNREDACTED_TRACE, false);
                             getRateLimiter().maybeUpdateRateLimiterDisabled(properties);
+
+                            String newTestPackageName = properties.getString(
+                                    DeviceConfigHelper.SYSTEM_TRIGGERED_TEST_PACKAGE_NAME, null);
+                            handleTestPackageChangeLocked(newTestPackageName);
                         }
                     }
                 });
@@ -340,9 +365,9 @@
                                     DeviceConfigHelper.REDACTION_MAX_RUNTIME_ALLOTTED_MS,
                                     mRedactionMaxRuntimeAllottedMs);
 
-                            mPersistQueueFrequencyMs.set(properties.getInt(
-                                    DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS,
-                                    mPersistQueueFrequencyMs.get()));
+                            mPersistFrequencyMs.set(properties.getInt(
+                                    DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS,
+                                    mPersistFrequencyMs.get()));
                         }
                     }
                 });
@@ -356,9 +381,9 @@
             }
         }, mClearTemporaryDirectoryBootDelayMs);
 
-        // Load the queue right away so we can start delivering results as apps register global
-        // listeners.
+        // Load the queue and triggers right away.
         loadQueueFromPersistedData();
+        loadAppTriggersFromPersistedData();
     }
 
     /**
@@ -446,17 +471,122 @@
         }
     }
 
+    /**
+     * Load persisted app triggers from disk.
+     *
+     * If any issue is encountered during loading, mark as completed and delete the file. Persisted
+     * app triggers will be lost.
+     */
+    @VisibleForTesting
+    public void loadAppTriggersFromPersistedData() {
+        // Setup persist files
+        try {
+            if (!setupPersistAppTriggerFiles()) {
+                // If setting up the directory and file was unsuccessful then just return without
+                // marking loaded so it can be tried again.
+                if (DEBUG) Log.d(TAG, "Failed to setup app trigger persistence directory/files.");
+                return;
+            }
+        } catch (SecurityException e) {
+            // Can't access files.
+            Log.w(TAG, "Failed to setup app trigger persistence directory/files.", e);
+            return;
+        }
+
+        // Check if file exists
+        try {
+            if (!mPersistAppTriggersFile.exists()) {
+                // No file, nothing to load. This is an expected state for before the feature has
+                // ever been used or if the triggers were empty.
+                if (DEBUG) {
+                    Log.d(TAG, "App trigger persistence file does not exist, skipping load from "
+                            + "disk.");
+                }
+                mAppTriggersLoaded = true;
+                return;
+            }
+        } catch (SecurityException e) {
+            // Can't access file.
+            if (DEBUG) Log.e(TAG, "Exception accessing app triggers persistence file", e);
+            return;
+        }
+
+        // Read the file
+        AtomicFile persistFile = new AtomicFile(mPersistAppTriggersFile);
+        byte[] bytes;
+        try {
+            bytes = persistFile.readFully();
+        } catch (IOException e) {
+            Log.w(TAG, "Exception reading app triggers persistence file", e);
+            // Failed to read the file. No reason to believe we'll have better luck next time,
+            // delete the file and return. Persisted triggers will be lost until the app re-adds
+            // them.
+            deletePersistAppTriggersFile();
+            mAppTriggersLoaded = true;
+            return;
+        }
+        if (bytes.length == 0) {
+            if (DEBUG) Log.d(TAG, "App triggers persistence file empty, skipping load from disk.");
+            // Empty app triggers persist file. Delete the file, mark loaded, and return.
+            deletePersistAppTriggersFile();
+            mAppTriggersLoaded = true;
+            return;
+        }
+
+        // Parse file bytes to proto
+        ProfilingTriggersWrapper wrapper;
+        try {
+            wrapper = ProfilingTriggersWrapper.parseFrom(bytes);
+        } catch (Exception e) {
+            Log.w(TAG, "Error parsing proto from persisted bytes", e);
+            // Failed to parse the file contents. No reason to believe we'll have better luck next
+            // time, delete the file, mark loaded, and return. Persisted app triggers will be lost
+            // until re-added by the app.
+            deletePersistAppTriggersFile();
+            mAppTriggersLoaded = true;
+            return;
+        }
+
+        // Populate in memory app triggers store
+        for (int i = 0; i < wrapper.getTriggersCount(); i++) {
+            ProfilingTriggersWrapper.ProfilingTrigger triggerProto = wrapper.getTriggers(i);
+            addTrigger(new ProfilingTrigger(triggerProto), false);
+        }
+
+        mAppTriggersLoaded = true;
+    }
+
     /** Setup the directory and file for persisting queue. */
     @VisibleForTesting
     public boolean setupPersistQueueFiles() {
-        File dataDir = Environment.getDataDirectory();
-        File systemDir = new File(dataDir, QUEUED_RESULTS_SYSTEM_DIR);
-        mPersistQueueStoreDir = new File(systemDir, QUEUED_RESULTS_STORE_DIR);
-        if (createDir(mPersistQueueStoreDir)) {
-            mPersistQueueFile = new File(mPersistQueueStoreDir, QUEUED_RESULTS_INFO_FILE);
-            return true;
+        if (mPersistStoreDir == null) {
+            if (!setupPersistDir()) {
+                return false;
+            }
         }
-        return false;
+        mPersistQueueFile = new File(mPersistStoreDir, QUEUED_RESULTS_INFO_FILE);
+        return true;
+    }
+
+    /** Setup the directory and file for persisting app triggers. */
+    @VisibleForTesting
+    public boolean setupPersistAppTriggerFiles() {
+        if (mPersistStoreDir == null) {
+            if (!setupPersistDir()) {
+                return false;
+            }
+        }
+        mPersistAppTriggersFile = new File(mPersistStoreDir, APP_TRIGGERS_INFO_FILE);
+        return true;
+    }
+
+    /** Setup the directory and file for persisting. */
+    @VisibleForTesting
+    public boolean setupPersistDir() {
+        File dataDir = Environment.getDataDirectory();
+        File systemDir = new File(dataDir, PERSIST_SYSTEM_DIR);
+        mPersistStoreDir = new File(systemDir, PERSIST_STORE_DIR);
+        return createDir(mPersistStoreDir);
     }
 
     /** Delete the persist queue file. */
@@ -471,6 +601,18 @@
         }
     }
 
+    /** Delete the persist app triggers file. */
+    @VisibleForTesting
+    public void deletePersistAppTriggersFile() {
+        try {
+            mPersistAppTriggersFile.delete();
+            if (DEBUG) Log.d(TAG, "Deleted app triggers persist file.");
+        } catch (SecurityException e) {
+            // Can't delete file.
+            if (DEBUG) Log.d(TAG, "Failed to delete app triggers persist file", e);
+        }
+    }
+
     private static boolean createDir(File dir) throws SecurityException {
         if (dir.mkdir()) {
             return true;
@@ -572,7 +714,7 @@
                 // NOTIFIED_REQUESTER and the only potential remaining work to be repeated will be
                 // cleanup. If the callback failed, then we won't have recursed here and we'll pick
                 // back up this stage next time thereby minimizing repeated work.
-                maybePersistQueueToDisk();
+                maybePersistToDisk();
                 break;
             case ERROR_OCCURRED:
                 // An error has occurred, proceed to callback.
@@ -584,7 +726,7 @@
                 // NOTIFIED_REQUESTER and the only potential remaining work to be repeated will be
                 // cleanup. If the callback failed, then we won't have recursed here and we'll pick
                 // back up this stage next time thereby minimizing repeated work.
-                maybePersistQueueToDisk();
+                maybePersistToDisk();
                 break;
             case NOTIFIED_REQUESTER:
                 // Callback has been completed successfully, start cleanup.
@@ -1191,11 +1333,19 @@
 
     /** Start a trace to be used for system triggered profiling. */
     private void startSystemTriggeredTrace() {
-        if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+        if (!Flags.systemTriggeredProfilingNew()) {
             // Flag disabled.
             return;
         }
 
+        if (!mAppTriggersLoaded) {
+            // Until the triggers are loaded we can't create a proper config so just return.
+            if (DEBUG) {
+                Log.d(TAG, "System triggered trace not started due to app triggers not loaded.");
+            }
+            return;
+        }
+
         String[] packageNames = getActiveTriggerPackageNames();
         if (packageNames.length == 0) {
             // No apps have registered interest in system triggered profiling, so don't bother to
@@ -1210,7 +1360,8 @@
         String uniqueSessionName = SYSTEM_TRIGGERED_SESSION_NAME_PREFIX
                 + System.currentTimeMillis();
 
-        byte[] config = Configs.generateSystemTriggeredTraceConfig(uniqueSessionName, packageNames);
+        byte[] config = Configs.generateSystemTriggeredTraceConfig(uniqueSessionName, packageNames,
+                mTestPackageName != null);
         String outputFile = TEMP_TRACE_PATH + SYSTEM_TRIGGERED_SESSION_NAME_PREFIX
                 + OUTPUT_FILE_IN_PROGRESS + OUTPUT_FILE_UNREDACTED_TRACE_SUFFIX;
 
@@ -1255,7 +1406,7 @@
      */
     @VisibleForTesting
     public void processTrigger(int uid, @NonNull String packageName, int triggerType) {
-        if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+        if (!Flags.systemTriggeredProfilingNew()) {
             // Flag disabled.
             return;
         }
@@ -1263,10 +1414,7 @@
         if (mSystemTriggeredTraceUniqueSessionName == null) {
             // If we don't have the session name then we don't know how to clone the trace so stop
             // it if it's still running and then return.
-            if (mSystemTriggeredTraceProcess != null && mSystemTriggeredTraceProcess.isAlive()) {
-                mSystemTriggeredTraceProcess.destroyForcibly();
-                mSystemTriggeredTraceProcess = null;
-            }
+            stopSystemTriggeredTrace();
 
             // There is no active system triggered trace so there's nothing to clone. Return.
             if (DEBUG) {
@@ -1318,17 +1466,20 @@
             return;
         }
 
-        int systemRateLimiterResult = getRateLimiter().isProfilingRequestAllowed(uid,
-                ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, true, null);
-        if (systemRateLimiterResult != RateLimiter.RATE_LIMIT_RESULT_ALLOWED) {
-            // Blocked by system rate limiter, return. Since this is system triggered there is no
-            // callback and therefore no need to distinguish between per app and system denials
-            // within the system rate limiter.
-            if (DEBUG) {
-                Log.d(TAG, String.format("Profiling triggered for uid %d and trigger %d but blocked"
-                        + " by system rate limiting ", uid, triggerType));
+        // If this is from the test package, skip system rate limiting.
+        if (!packageName.equals(mTestPackageName)) {
+            int systemRateLimiterResult = getRateLimiter().isProfilingRequestAllowed(uid,
+                    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, true, null);
+            if (systemRateLimiterResult != RateLimiter.RATE_LIMIT_RESULT_ALLOWED) {
+                // Blocked by system rate limiter, return. Since this is system triggered there is
+                // no callback and therefore no need to distinguish between per app and system
+                // denials within the system rate limiter.
+                if (DEBUG) {
+                    Log.d(TAG, String.format("Profiling triggered for uid %d and trigger %d but "
+                            + "blocked by system rate limiting ", uid, triggerType));
+                }
+                return;
             }
-            return;
         }
 
         // Now that it's approved by both rate limiters, update their values.
@@ -1346,13 +1497,30 @@
 
         try {
             // Try to clone the running trace.
-            Runtime.getRuntime().exec(new String[] {
+            Process clone = Runtime.getRuntime().exec(new String[] {
                     "/system/bin/perfetto",
                     "--clone-by-name",
                     mSystemTriggeredTraceUniqueSessionName,
                     "--out",
                     TEMP_TRACE_PATH + unredactedFullName});
-        } catch (IOException e) {
+
+            // Wait for cloned process to stop.
+            if (!clone.waitFor(mPerfettoDestroyTimeoutMs, TimeUnit.MILLISECONDS)) {
+                // Cloned process did not stop, try to stop it forcibly.
+                if (DEBUG) {
+                    Log.d(TAG, "Cloned system triggered trace didn't stop on its own, trying to "
+                            + "stop it forcibly.");
+                }
+                clone.destroyForcibly();
+
+                // Wait again to see if it stops now.
+                if (!clone.waitFor(mPerfettoDestroyTimeoutMs, TimeUnit.MILLISECONDS)) {
+                    // Nothing more to do, result won't be ready so return.
+                    if (DEBUG) Log.d(TAG, "Cloned system triggered trace timed out.");
+                    return;
+                }
+            }
+        } catch (IOException | InterruptedException e) {
             // Failed. There's nothing to clean up as we haven't created a session for this clone
             // yet so just fail quietly. The result for this trigger instance combo will be lost.
             if (DEBUG) Log.d(TAG, "Failed to clone running system triggered trace.", e);
@@ -1367,30 +1535,47 @@
         session.setFileName(unredactedFullName);
         moveSessionToQueue(session, true);
         advanceTracingSession(session, TracingState.PROFILING_FINISHED);
+
+        maybePersistToDisk();
     }
 
     /** Add a profiling trigger to the supporting data structure. */
     @VisibleForTesting
     public void addTrigger(int uid, @NonNull String packageName, int triggerType,
             int rateLimitingPeriodHours) {
-        if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+        addTrigger(new ProfilingTrigger(uid, packageName, triggerType, rateLimitingPeriodHours),
+                true);
+    }
+
+    /**
+     * Add a profiling trigger to the supporting data structure.
+     *
+     * @param trigger       The trigger to add.
+     * @param maybePersist  Whether to persist to disk, if eligible based on frequency. This is
+     *                          intended to be set to false only when loading triggers from disk.
+     */
+    @VisibleForTesting
+    public void addTrigger(ProfilingTrigger trigger, boolean maybePersist) {
+        if (!Flags.systemTriggeredProfilingNew()) {
             // Flag disabled.
             return;
         }
 
-        ProfilingTrigger trigger = new ProfilingTrigger(
-                uid, packageName, triggerType, rateLimitingPeriodHours);
-
-        SparseArray<ProfilingTrigger> perProcessTriggers = mAppTriggers.get(packageName, uid);
+        SparseArray<ProfilingTrigger> perProcessTriggers = mAppTriggers.get(
+                trigger.getPackageName(), trigger.getUid());
 
         if (perProcessTriggers == null) {
             perProcessTriggers = new SparseArray<ProfilingTrigger>();
-            mAppTriggers.put(packageName, uid, perProcessTriggers);
+            mAppTriggers.put(trigger.getPackageName(), trigger.getUid(), perProcessTriggers);
         }
 
         // Only 1 trigger is allowed per uid + trigger type so this will override any previous
         // triggers of this type registered for this uid.
-        perProcessTriggers.put(triggerType, trigger);
+        perProcessTriggers.put(trigger.getTriggerType(), trigger);
+
+        if (maybePersist) {
+            maybePersistToDisk();
+        }
     }
 
     /** Get a list of all package names which have registered profiling triggers. */
@@ -1883,7 +2068,7 @@
         mActiveTracingSessions.remove(session.getKey());
 
         if (maybePersist) {
-            maybePersistQueueToDisk();
+            maybePersistToDisk();
         }
     }
 
@@ -1964,49 +2149,60 @@
     }
 
     /**
-     * Persist queued results to disk following the following rules:
+     * Persist service data to disk following the following rules:
      * - If a persist is already scheduled, do nothing.
-     * - If a persist happened within the last {@link #mPersistQueueFrequencyMs} then schedule a
-     *      persist for {@link #mPersistQueueFrequencyMs} after the last persist.
+     * - If a persist happened within the last {@link #mPersistFrequencyMs} then schedule a
+     *      persist for {@link #mPersistFrequencyMs} after the last persist.
      * - If no persist has occurred yet or the most recent persist was more than
-     *      {@link #mPersistQueueFrequencyMs} ago, persist immediately.
+     *      {@link #mPersistFrequencyMs} ago, persist immediately.
      */
     @VisibleForTesting
-    public void maybePersistQueueToDisk() {
-        if (!Flags.persistQueue()) {
+    public void maybePersistToDisk() {
+        if (!Flags.persistQueue() && !Flags.systemTriggeredProfilingNew()) {
+            // No persisting is enabled.
             return;
         }
 
         synchronized (mLock) {
-            if (mPersistQueueScheduled) {
+            if (mPersistScheduled) {
                 // We're already waiting on a scheduled persist job, do nothing.
                 return;
             }
 
-            if (mPersistQueueFrequencyMs.get() != 0
-                    && (System.currentTimeMillis() - mLastPersistedQueueTimestampMs
-                    < mPersistQueueFrequencyMs.get())) {
+            if (mPersistFrequencyMs.get() != 0
+                    && (System.currentTimeMillis() - mLastPersistedTimestampMs
+                    < mPersistFrequencyMs.get())) {
                 // Schedule the persist job.
-                if (mPersistQueueRunnable == null) {
-                    mPersistQueueRunnable = new Runnable() {
+                if (mPersistRunnable == null) {
+                    mPersistRunnable = new Runnable() {
                         @Override
                         public void run() {
-                            persistQueueToDisk();
-                            mPersistQueueScheduled = false;
+                            if (Flags.persistQueue()) {
+                                persistQueueToDisk();
+                            }
+                            if (Flags.systemTriggeredProfilingNew()) {
+                                persistAppTriggersToDisk();
+                            }
+                            mPersistScheduled = false;
                         }
                     };
                 }
-                mPersistQueueScheduled = true;
-                long persistDelay = mLastPersistedQueueTimestampMs + mPersistQueueFrequencyMs.get()
+                mPersistScheduled = true;
+                long persistDelay = mLastPersistedTimestampMs + mPersistFrequencyMs.get()
                         - System.currentTimeMillis();
-                getHandler().postDelayed(mPersistQueueRunnable, persistDelay);
+                getHandler().postDelayed(mPersistRunnable, persistDelay);
                 return;
             }
         }
 
         // If we got here then either persist frequency is 0 or it has already been longer than
         // persist frequency since the last persist. Persist immediately.
-        persistQueueToDisk();
+        if (Flags.persistQueue()) {
+            persistQueueToDisk();
+        }
+        if (Flags.systemTriggeredProfilingNew()) {
+            persistAppTriggersToDisk();
+        }
     }
 
     /** Persist the current queue to disk after cleaning it up. */
@@ -2077,7 +2273,7 @@
             out.write(protoBytes);
             persistFile.finishWrite(out);
             synchronized (mLock) {
-                mLastPersistedQueueTimestampMs = System.currentTimeMillis();
+                mLastPersistedTimestampMs = System.currentTimeMillis();
             }
         } catch (IOException e) {
             if (DEBUG) Log.e(TAG, "Exception writing queued results", e);
@@ -2085,6 +2281,125 @@
         }
     }
 
+    /** Persist the current app triggers to disk. */
+    @VisibleForTesting
+    public void persistAppTriggersToDisk() {
+        // Check if file exists
+        try {
+            if (mPersistAppTriggersFile == null) {
+                // Try again to create the necessary files.
+                if (!setupPersistAppTriggerFiles()) {
+                    // No file, nowhere to save.
+                    if (DEBUG) {
+                        Log.d(TAG, "Failed setting up app triggers persist files so nowhere to save"
+                                + " to.");
+                    }
+                    return;
+                }
+            }
+
+            if (!mPersistAppTriggersFile.exists()) {
+                // File doesn't exist, try to create it.
+                mPersistAppTriggersFile.createNewFile();
+            }
+        } catch (Exception e) {
+            if (DEBUG) Log.e(TAG, "Exception accessing persisted app triggers store.", e);
+            return;
+        }
+
+        // Generate proto for queue.
+        ProfilingTriggersWrapper.Builder builder = ProfilingTriggersWrapper.newBuilder();
+
+        forEachTrigger(mAppTriggers.getMap(), (trigger) -> builder.addTriggers(trigger.toProto()));
+
+        ProfilingTriggersWrapper queuedTriggersWrapper = builder.build();
+
+        // Write to disk
+        byte[] protoBytes = queuedTriggersWrapper.toByteArray();
+        AtomicFile persistFile = new AtomicFile(mPersistAppTriggersFile);
+        FileOutputStream out = null;
+        try {
+            out = persistFile.startWrite();
+            out.write(protoBytes);
+            persistFile.finishWrite(out);
+            synchronized (mLock) {
+                mLastPersistedTimestampMs = System.currentTimeMillis();
+            }
+        } catch (IOException e) {
+            if (DEBUG) Log.e(TAG, "Exception writing app triggers", e);
+            persistFile.failWrite(out);
+        }
+    }
+
+    /** Receive a callback with each of the tracked profiling triggers. */
+    private void forEachTrigger(
+            ArrayMap<String, SparseArray<SparseArray<ProfilingTrigger>>> triggersOuterMap,
+            Consumer<ProfilingTrigger> callback) {
+
+        for (int i = 0; i < triggersOuterMap.size(); i++) {
+            SparseArray<SparseArray<ProfilingTrigger>> triggerUidList = triggersOuterMap.valueAt(i);
+
+            for (int j = 0; j < triggerUidList.size(); j++) {
+                int uidKey = triggerUidList.keyAt(j);
+                SparseArray<ProfilingTrigger> triggersList = triggerUidList.get(uidKey);
+
+                if (triggersList != null) {
+                    for (int k = 0; k < triggersList.size(); k++) {
+                        int triggerTypeKey = triggersList.keyAt(k);
+                        ProfilingTrigger trigger = triggersList.get(triggerTypeKey);
+
+                        if (trigger != null) {
+                            callback.accept(trigger);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /** Handle updates to test package config value. */
+    @GuardedBy("mLock")
+    private void handleTestPackageChangeLocked(String newTestPackageName) {
+        if (newTestPackageName == null) {
+
+            // Test package has been set to null, check whether it was null previously.
+            if (mTestPackageName != null) {
+
+                // New null state is a changed from previous state, disable test mode.
+                mTestPackageName = null;
+                stopSystemTriggeredTrace();
+            }
+            // If new state is unchanged from previous null state, do nothing.
+        } else {
+
+            // Test package has been set with a value. Stop running system triggered trace if
+            // applicable so we can start a new one that will have most up to date package names.
+            // This should not be called when the new test package name matches the old one as
+            // device config should not be sending an update for a value change when the value
+            // remains the same, but no need to check as the best experience for caller is to always
+            // stop the current trace and start a new one for most up to date package list.
+            stopSystemTriggeredTrace();
+
+            // Now update the test package name and start the system triggered trace.
+            mTestPackageName = newTestPackageName;
+            startSystemTriggeredTrace();
+        }
+    }
+
+    /** Stop the system triggered trace. */
+    private void stopSystemTriggeredTrace() {
+        // If the trace is alive, stop it.
+        if (mSystemTriggeredTraceProcess != null) {
+            if (mSystemTriggeredTraceProcess.isAlive()) {
+                mSystemTriggeredTraceProcess.destroyForcibly();
+            }
+            mSystemTriggeredTraceProcess = null;
+        }
+
+        // Set session name to null.
+        mSystemTriggeredTraceUniqueSessionName = null;
+    }
+
     private class ProfilingDeathRecipient implements IBinder.DeathRecipient {
         private final int mUid;
 
diff --git a/service/java/com/android/os/profiling/RateLimiter.java b/service/java/com/android/os/profiling/RateLimiter.java
index 0038aa0..a57b840 100644
--- a/service/java/com/android/os/profiling/RateLimiter.java
+++ b/service/java/com/android/os/profiling/RateLimiter.java
@@ -177,8 +177,9 @@
     public @RateLimitResult int isProfilingRequestAllowed(int uid,
             int profilingType, boolean isTriggered, @Nullable Bundle params) {
         synchronized (mLock) {
-            if (mRateLimiterDisabled) {
+            if (mRateLimiterDisabled && !isTriggered) {
                 // Rate limiter is disabled for testing, approve request and don't store cost.
+                // This mechanism applies only to direct requests, not system triggered ones.
                 Log.w(TAG, "Rate limiter disabled, request allowed.");
                 return RATE_LIMIT_RESULT_ALLOWED;
             }
diff --git a/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java b/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
index 5770137..ecd91b5 100644
--- a/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
+++ b/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
@@ -90,6 +90,9 @@
     private static final String OVERRIDE_DEVICE_CONFIG_INT = "device_config put %s %s %d";
     private static final String GET_DEVICE_CONFIG = "device_config get %s %s";
 
+    private static final String PERSIST_TEST_DIR = "testdir";
+    private static final String PERSIST_TEST_FILE = "testfile";
+
     // Key most and least significant bits are used to generate a unique key specific to each
     // request. Key is used to pair request back to caller and callbacks so test to keep consistent.
     private static final long KEY_MOST_SIG_BITS = 456l;
@@ -127,15 +130,20 @@
         // to our own file/directory in app storage, since the test app context can't access
         // /data/system
         doReturn(true).when(mRateLimiter).setupPersistFiles();
-        mRateLimiter.mPersistStoreDir = new File(mContext.getFilesDir(), "testdir");
+        mRateLimiter.mPersistStoreDir = new File(mContext.getFilesDir(), PERSIST_TEST_DIR);
         mRateLimiter.mPersistStoreDir.mkdir();
-        mRateLimiter.mPersistFile = new File(mRateLimiter.mPersistStoreDir, "testfile");
+        mRateLimiter.mPersistFile = new File(mRateLimiter.mPersistStoreDir, PERSIST_TEST_FILE);
 
         doReturn(true).when(mProfilingService).setupPersistQueueFiles();
-        mProfilingService.mPersistQueueStoreDir = new File(mContext.getFilesDir(), "testdir");
+        mProfilingService.mPersistStoreDir =
+                new File(mContext.getFilesDir(), PERSIST_TEST_DIR);
         // Same dir for both, no need to create the 2nd time.
         mProfilingService.mPersistQueueFile =
-                new File(mProfilingService.mPersistQueueStoreDir, "testfile");
+                new File(mProfilingService.mPersistStoreDir, PERSIST_TEST_FILE);
+
+        doReturn(true).when(mProfilingService).setupPersistAppTriggerFiles();
+        mProfilingService.mPersistAppTriggersFile =
+                new File(mProfilingService.mPersistStoreDir, PERSIST_TEST_FILE);
     }
 
     @After
@@ -908,13 +916,16 @@
     /**
      * Test that persisting queue respects the frequency defined, allowing the persist on the first
      * instance but rejecting the subsequent persist.
+     *
+     * While this test focuses on queue persist, the logic for respect frequency is shared with
+     * triggers so this test covers both.
      */
     @Test
     @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
     public void testQueuePersist_RespectFrequency() throws Exception {
         // Override persist frequency to something large.
         updateDeviceConfigAndWaitForChange(DeviceConfigHelper.NAMESPACE,
-                DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS, 60 * 60 * 1000);
+                DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS, 60 * 60 * 1000);
 
         // Clear the queue.
         mProfilingService.mQueuedTracingResults.clear();
@@ -948,7 +959,7 @@
         mProfilingService.mQueuedTracingResults.put(FAKE_UID, sessionList);
 
         // Trigger a persist.
-        mProfilingService.maybePersistQueueToDisk();
+        mProfilingService.maybePersistToDisk();
 
         // Confirm that it actually persisted.
         verify(mProfilingService, times(1)).persistQueueToDisk();
@@ -958,7 +969,7 @@
         assertTrue(mProfilingService.mPersistQueueFile.delete());
 
         // Finally, trigger another persist.
-        mProfilingService.maybePersistQueueToDisk();
+        mProfilingService.maybePersistToDisk();
 
         // And confirm the persist did not immediately run.
         assertFalse(mProfilingService.mPersistQueueFile.exists());
@@ -969,16 +980,19 @@
     /**
      * Test that persists that are scheduled for the future due to a persist having recently
      * occurred, occur at a future time as expected.
+     *
+     * While this test focuses on queue persist, the logic for scheduling is shared with triggers so
+     * this test covers both.
      */
     @Test
     @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
     public void testQueuePersist_Scheduling() throws Exception {
         // Override persist frequency to 5 seconds that way we can confirm both that the persist did
         // not happen immediately and that it did eventually happen. This is the time from the first
-        // call to maybePersistQueueToDisk until the next call to the same method for the scheduling
+        // call to maybePersistToDisk until the next call to the same method for the scheduling
         // of the next persist to occur as expected, rather than immediately persisting.
         updateDeviceConfigAndWaitForChange(DeviceConfigHelper.NAMESPACE,
-                DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS, 5 * 1000);
+                DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS, 5 * 1000);
 
         // Clear the queue.
         mProfilingService.mQueuedTracingResults.clear();
@@ -1000,7 +1014,7 @@
         mProfilingService.mQueuedTracingResults.put(FAKE_UID, sessionList);
 
         // Trigger a persist.
-        mProfilingService.maybePersistQueueToDisk();
+        mProfilingService.maybePersistToDisk();
 
         // Confirm that it actually persisted.
         assertTrue(mProfilingService.mPersistQueueFile.exists());
@@ -1010,7 +1024,7 @@
         assertTrue(mProfilingService.mPersistQueueFile.delete());
 
         // Trigger another persist.
-        mProfilingService.maybePersistQueueToDisk();
+        mProfilingService.maybePersistToDisk();
 
         // And confirm the persist did not immediately run.
         assertFalse(mProfilingService.mPersistQueueFile.exists());
@@ -1022,6 +1036,137 @@
         assertTrue(mProfilingService.mPersistQueueFile.exists());
     }
 
+    /**
+     * Test that persisting app triggers and then reloading them from disk works correctly, loading
+     * all previous triggers.
+     */
+    @Test
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+    public void testAppTriggersPersist_PersistAndRestore() {
+        // First, clear the data structure.
+        mProfilingService.mAppTriggers.getMap().clear();
+
+        // Create 3 triggers belonging to 2 uids. Add a last triggered time to one of them.
+        ProfilingTrigger trigger1 = new ProfilingTrigger(FAKE_UID, APP_PACKAGE_NAME, 1, 0);
+
+        ProfilingTrigger trigger2 = new ProfilingTrigger(FAKE_UID, APP_PACKAGE_NAME, 2, 1);
+        trigger2.setLastTriggeredTimeMs(123L);
+
+        ProfilingTrigger trigger3 = new ProfilingTrigger(FAKE_UID_2, APP_PACKAGE_NAME, 1, 2);
+
+        // Group into sparse arrays by uid.
+        SparseArray<ProfilingTrigger> triggerArray1 = new SparseArray<ProfilingTrigger>();
+        triggerArray1.put(1, trigger1);
+        triggerArray1.put(2, trigger2);
+
+        SparseArray<ProfilingTrigger> triggerArray2 = new SparseArray<ProfilingTrigger>();
+        triggerArray2.put(1, trigger3);
+
+        mProfilingService.mAppTriggers.put(APP_PACKAGE_NAME, FAKE_UID, triggerArray1);
+        mProfilingService.mAppTriggers.put(APP_PACKAGE_NAME, FAKE_UID_2, triggerArray2);
+
+        // Trigger a persist.
+        mProfilingService.persistAppTriggersToDisk();
+
+        // Confirm file was written to
+        confirmNonEmptyFileExists(mProfilingService.mPersistAppTriggersFile);
+
+        // Clear app triggers so we can ensure it is reloaded properly.
+        mProfilingService.mAppTriggers.getMap().clear();
+        assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+
+        // Load app triggers from disk.
+        mProfilingService.loadAppTriggersFromPersistedData();
+
+        // Finally, verify the loaded contents match the ones that were persisted.
+        confirmProfilingTriggerEquals(trigger1,
+                mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID).get(1));
+        confirmProfilingTriggerEquals(trigger2,
+                mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID).get(2));
+        confirmProfilingTriggerEquals(trigger3,
+                mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID_2).get(1));
+    }
+
+    /**
+     * Test that loading app triggers with no persist file works as intended with no triggers added,
+     * loaded set to true, and correct methods called.
+     */
+    @Test
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+    public void testAppTriggersPersist_NoPersistFile() {
+        // First, clear the data structure.
+        mProfilingService.mAppTriggers.getMap().clear();
+
+        // Ensure the file doesn't exist.
+        mProfilingService.mPersistAppTriggersFile.delete();
+        assertFalse(mProfilingService.mPersistAppTriggersFile.exists());
+
+        // Load app triggers from disk.
+        mProfilingService.loadAppTriggersFromPersistedData();
+
+        // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+        // was not attempted as there was no file to delete.
+        assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+        assertTrue(mProfilingService.mAppTriggersLoaded);
+        verify(mProfilingService, times(0)).deletePersistAppTriggersFile();
+    }
+
+    /**
+     * Test that loading app triggers with an empty persist file works as intended with no triggers
+     * added, loaded set to true, and correct methods called.
+     */
+    @Test
+    @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
+    public void testAppTriggersPersist_EmptyPersistFile() throws Exception {
+        // First, clear the data structure.
+        mProfilingService.mAppTriggers.getMap().clear();
+
+        // Ensure the file exists and is empty.
+        mProfilingService.mPersistAppTriggersFile.delete();
+        assertFalse(mProfilingService.mPersistAppTriggersFile.exists());
+        mProfilingService.mPersistAppTriggersFile.createNewFile();
+        assertTrue(mProfilingService.mPersistAppTriggersFile.exists());
+        assertEquals(0L, mProfilingService.mPersistAppTriggersFile.length());
+
+        // Load app triggers from disk.
+        mProfilingService.loadAppTriggersFromPersistedData();
+
+        // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+        // was attempted as expected for the bad file state.
+        assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+        assertTrue(mProfilingService.mAppTriggersLoaded);
+        verify(mProfilingService, times(1)).deletePersistAppTriggersFile();
+    }
+
+    /**
+     * Test that loading app triggers with an invalid persist file works as intended with no
+     * triggers added, loaded set to true, and correct methods called.
+     */
+    @Test
+    @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
+    public void testAppTriggersPersist_BadPersistFile() throws Exception {
+        // First, clear the data structure.
+        mProfilingService.mAppTriggers.getMap().clear();
+
+        // Ensure the file exists and contains some non proto contents.
+        mProfilingService.mPersistAppTriggersFile.delete();
+        mProfilingService.mPersistAppTriggersFile.createNewFile();
+        FileOutputStream fileOutputStream = new FileOutputStream(
+                mProfilingService.mPersistAppTriggersFile);
+        fileOutputStream.write("some text that is definitely not a proto".getBytes());
+        fileOutputStream.close();
+        confirmNonEmptyFileExists(mProfilingService.mPersistAppTriggersFile);
+
+        // Load app triggers from disk.
+        mProfilingService.loadAppTriggersFromPersistedData();
+
+        // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+        // was attempted as expected for the bad file state.
+        assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+        assertTrue(mProfilingService.mAppTriggersLoaded);
+        verify(mProfilingService, times(1)).deletePersistAppTriggersFile();
+    }
+
     /** Test that adding a specific listener does not trigger handling queued results. */
     @Test
     public void testQueuedResult_RequestSpecificListener() {
@@ -1543,7 +1688,7 @@
      * the same trigger, uid, and process name are used.
      */
     @Test
-    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
     public void testAddTriggers() throws Exception {
         // First, clear the data structure.
         mProfilingService.mAppTriggers.getMap().clear();
@@ -1579,7 +1724,7 @@
 
     /** Test that app level rate limiting works correctly in the allow case. */
     @Test
-    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
     public void testProcessTrigger_appLevelRateLimit_allow() throws Exception {
         // First, clear the data structure.
         mProfilingService.mAppTriggers.getMap().clear();
@@ -1621,7 +1766,7 @@
 
     /** Test that app level rate limiting works correctly in the deny case. */
     @Test
-    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
     public void testProcessTrigger_appLevelRateLimit_deny() throws Exception {
         // First, clear the data structure.
         mProfilingService.mAppTriggers.getMap().clear();
@@ -1664,7 +1809,7 @@
 
     /** Test that system level rate limiting works correctly in the allow case. */
     @Test
-    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
     public void testProcessTrigger_systemLevelRateLimit_allow() throws Exception {
         overrideRateLimiterDefaults();
 
@@ -1697,7 +1842,7 @@
 
     /** Test that system level rate limiting works correctly in the deny case. */
     @Test
-    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+    @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
     public void testProcessTrigger_systemLevelRateLimit_deny() throws Exception {
         overrideRateLimiterDefaults();