Separate job service for dynamic code logging.

Decouple logging of dynamic code loading from background DEX
optimisation/reconciliation (BackgroundDexOptService).

Add DynamicCodeLoggingService to run DCL logging daily while idle and
charging.

Update DexLoggerIntegrationTests to use the new job, and to verify
that we now handle unknown class loaders.

Bug: 111336847
Test: atest -p services/core/java/com/android/server/pm/dex
Change-Id: Id688a7eef5976120be12606e726830ce32451a1e
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ff73df6..d9e6240 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4685,6 +4685,10 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.server.pm.DynamicCodeLoggingService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
         <service android:name="com.android.server.PruneInstantAppsJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
new file mode 100644
index 0000000..2ae424d
--- /dev/null
+++ b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.server.pm.dex.DexLogger;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Scheduled job to trigger logging of app dynamic code loading. This runs daily while idle and
+ * charging. The actual logging is performed by {@link DexLogger}.
+ * {@hide}
+ */
+public class DynamicCodeLoggingService extends JobService {
+    private static final String TAG = DynamicCodeLoggingService.class.getName();
+
+    private static final int JOB_ID = 2030028;
+    private static final long PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
+
+    private volatile boolean mStopRequested = false;
+
+    private static final boolean DEBUG = false;
+
+    /**
+     * Schedule our job with the {@link JobScheduler}.
+     */
+    public static void schedule(Context context) {
+        ComponentName serviceName = new ComponentName(
+                "android", DynamicCodeLoggingService.class.getName());
+
+        JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        js.schedule(new JobInfo.Builder(JOB_ID, serviceName)
+                .setRequiresDeviceIdle(true)
+                .setRequiresCharging(true)
+                .setPeriodic(PERIOD_MILLIS)
+                .build());
+        if (DEBUG) {
+            Log.d(TAG, "Job scheduled");
+        }
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        if (DEBUG) {
+            Log.d(TAG, "onStartJob");
+        }
+        mStopRequested = false;
+        new IdleLoggingThread(params).start();
+        return true;  // Job is running on another thread
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        if (DEBUG) {
+            Log.d(TAG, "onStopJob");
+        }
+        mStopRequested = true;
+        return true;  // Requests job be re-scheduled.
+    }
+
+    private class IdleLoggingThread extends Thread {
+        private final JobParameters mParams;
+
+        IdleLoggingThread(JobParameters params) {
+            super("DynamicCodeLoggingService_IdleLoggingJob");
+            mParams = params;
+        }
+
+        @Override
+        public void run() {
+            if (DEBUG) {
+                Log.d(TAG, "Starting logging run");
+            }
+
+            PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
+            DexLogger dexLogger = pm.getDexManager().getDexLogger();
+            for (String packageName : dexLogger.getAllPackagesWithDynamicCodeLoading()) {
+                if (mStopRequested) {
+                    Log.w(TAG, "Stopping logging run at scheduler request");
+                    return;
+                }
+
+                dexLogger.logDynamicCodeLoading(packageName);
+            }
+
+            jobFinished(mParams, /* reschedule */ false);
+            if (DEBUG) {
+                Log.d(TAG, "Finished logging run");
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index edab94c..9aebee0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -304,7 +304,6 @@
 import com.android.server.pm.Settings.DatabaseVersion;
 import com.android.server.pm.Settings.VersionInfo;
 import com.android.server.pm.dex.ArtManagerService;
-import com.android.server.pm.dex.DexLogger;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.dex.PackageDexUsage;
@@ -2168,10 +2167,7 @@
 
         mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
                 "*dexopt*");
-        DexManager.Listener dexManagerListener = DexLogger.getListener(this,
-                installer, mInstallLock);
-        mDexManager = new DexManager(mContext, this, mPackageDexOptimizer, installer, mInstallLock,
-                dexManagerListener);
+        mDexManager = new DexManager(mContext, this, mPackageDexOptimizer, installer, mInstallLock);
         mArtManagerService = new ArtManagerService(mContext, this, installer, mInstallLock);
         mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
 
@@ -9215,7 +9211,7 @@
 
     /**
      * Reconcile the information we have about the secondary dex files belonging to
-     * {@code packagName} and the actual dex files. For all dex files that were
+     * {@code packageName} and the actual dex files. For all dex files that were
      * deleted, update the internal records and delete the generated oat files.
      */
     @Override
diff --git a/services/core/java/com/android/server/pm/dex/DexLogger.java b/services/core/java/com/android/server/pm/dex/DexLogger.java
index 88d9e52..68a755b 100644
--- a/services/core/java/com/android/server/pm/dex/DexLogger.java
+++ b/services/core/java/com/android/server/pm/dex/DexLogger.java
@@ -18,29 +18,32 @@
 
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.os.FileUtils;
 import android.os.RemoteException;
-
-import android.util.ArraySet;
+import android.os.storage.StorageManager;
 import android.util.ByteStringUtils;
 import android.util.EventLog;
 import android.util.PackageUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.pm.Installer;
 import com.android.server.pm.Installer.InstallerException;
+import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
 
 import java.io.File;
+import java.util.Map;
 import java.util.Set;
 
-import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
-
 /**
  * This class is responsible for logging data about secondary dex files.
  * The data logged includes hashes of the name and content of each file.
  */
-public class DexLogger implements DexManager.Listener {
+public class DexLogger {
     private static final String TAG = "DexLogger";
 
     // Event log tag & subtag used for SafetyNet logging of dynamic
@@ -49,75 +52,172 @@
     private static final String DCL_SUBTAG = "dcl";
 
     private final IPackageManager mPackageManager;
+    private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
     private final Object mInstallLock;
     @GuardedBy("mInstallLock")
     private final Installer mInstaller;
 
-    public static DexManager.Listener getListener(IPackageManager pms,
-            Installer installer, Object installLock) {
-        return new DexLogger(pms, installer, installLock);
+    public DexLogger(IPackageManager pms, Installer installer, Object installLock) {
+        this(pms, installer, installLock, new PackageDynamicCodeLoading());
     }
 
     @VisibleForTesting
-    /*package*/ DexLogger(IPackageManager pms, Installer installer, Object installLock) {
+    DexLogger(IPackageManager pms, Installer installer, Object installLock,
+            PackageDynamicCodeLoading packageDynamicCodeLoading) {
         mPackageManager = pms;
+        mPackageDynamicCodeLoading = packageDynamicCodeLoading;
         mInstaller = installer;
         mInstallLock = installLock;
     }
 
-    /**
-     * Compute and log hashes of the name and content of a secondary dex file.
-     */
-    @Override
-    public void onReconcileSecondaryDexFile(ApplicationInfo appInfo, DexUseInfo dexUseInfo,
-            String dexPath, int storageFlags) {
-        int ownerUid = appInfo.uid;
+    public Set<String> getAllPackagesWithDynamicCodeLoading() {
+        return mPackageDynamicCodeLoading.getAllPackagesWithDynamicCodeLoading();
+    }
 
-        byte[] hash = null;
-        synchronized(mInstallLock) {
-            try {
-                hash = mInstaller.hashSecondaryDexFile(dexPath, appInfo.packageName,
-                        ownerUid, appInfo.volumeUuid, storageFlags);
-            } catch (InstallerException e) {
-                Slog.e(TAG, "Got InstallerException when hashing dex " + dexPath +
-                        " : " + e.getMessage());
-            }
-        }
-        if (hash == null) {
+    /**
+     * Write information about code dynamically loaded by {@code packageName} to the event log.
+     */
+    public void logDynamicCodeLoading(String packageName) {
+        PackageDynamicCode info = getPackageDynamicCodeInfo(packageName);
+        if (info == null) {
             return;
         }
 
-        String dexFileName = new File(dexPath).getName();
-        String message = PackageUtils.computeSha256Digest(dexFileName.getBytes());
-        // Valid SHA256 will be 256 bits, 32 bytes.
-        if (hash.length == 32) {
-            message = message + ' ' + ByteStringUtils.toHexString(hash);
-        }
+        SparseArray<ApplicationInfo> appInfoByUser = new SparseArray<>();
+        boolean needWrite = false;
 
-        writeDclEvent(ownerUid, message);
+        for (Map.Entry<String, DynamicCodeFile> fileEntry : info.mFileUsageMap.entrySet()) {
+            String filePath = fileEntry.getKey();
+            DynamicCodeFile fileInfo = fileEntry.getValue();
+            int userId = fileInfo.mUserId;
 
-        if (dexUseInfo.isUsedByOtherApps()) {
-            Set<String> otherPackages = dexUseInfo.getLoadingPackages();
-            Set<Integer> otherUids = new ArraySet<>(otherPackages.size());
-            for (String otherPackageName : otherPackages) {
+            int index = appInfoByUser.indexOfKey(userId);
+            ApplicationInfo appInfo;
+            if (index >= 0) {
+                appInfo = appInfoByUser.get(userId);
+            } else {
+                appInfo = null;
+
                 try {
-                    int otherUid = mPackageManager.getPackageUid(
-                        otherPackageName, /*flags*/0, dexUseInfo.getOwnerUserId());
-                    if (otherUid != -1 && otherUid != ownerUid) {
-                        otherUids.add(otherUid);
-                    }
-                } catch (RemoteException ignore) {
+                    PackageInfo ownerInfo =
+                            mPackageManager.getPackageInfo(packageName, /*flags*/ 0, userId);
+                    appInfo = ownerInfo == null ? null : ownerInfo.applicationInfo;
+                } catch (RemoteException ignored) {
                     // Can't happen, we're local.
                 }
+                appInfoByUser.put(userId, appInfo);
+                if (appInfo == null) {
+                    Slog.d(TAG, "Could not find package " + packageName + " for user " + userId);
+                    // Package has probably been uninstalled for user.
+                    needWrite |= mPackageDynamicCodeLoading.removeUserPackage(packageName, userId);
+                }
             }
-            for (int otherUid : otherUids) {
-                writeDclEvent(otherUid, message);
+
+            if (appInfo == null) {
+                continue;
             }
+
+            int storageFlags;
+            if (appInfo.deviceProtectedDataDir != null
+                    && FileUtils.contains(appInfo.deviceProtectedDataDir, filePath)) {
+                storageFlags = StorageManager.FLAG_STORAGE_DE;
+            } else if (appInfo.credentialProtectedDataDir != null
+                    && FileUtils.contains(appInfo.credentialProtectedDataDir, filePath)) {
+                storageFlags = StorageManager.FLAG_STORAGE_CE;
+            } else {
+                Slog.e(TAG, "Could not infer CE/DE storage for path " + filePath);
+                needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
+                continue;
+            }
+
+            byte[] hash = null;
+            synchronized (mInstallLock) {
+                try {
+                    hash = mInstaller.hashSecondaryDexFile(filePath, packageName, appInfo.uid,
+                            appInfo.volumeUuid, storageFlags);
+                } catch (InstallerException e) {
+                    Slog.e(TAG, "Got InstallerException when hashing file " + filePath
+                            + ": " + e.getMessage());
+                }
+            }
+
+            String fileName = new File(filePath).getName();
+            String message = PackageUtils.computeSha256Digest(fileName.getBytes());
+
+            // Valid SHA256 will be 256 bits, 32 bytes.
+            if (hash != null && hash.length == 32) {
+                message = message + ' ' + ByteStringUtils.toHexString(hash);
+            } else {
+                Slog.d(TAG, "Got no hash for " + filePath);
+                // File has probably been deleted.
+                needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
+            }
+
+            for (String loadingPackageName : fileInfo.mLoadingPackages) {
+                int loadingUid = -1;
+                if (loadingPackageName.equals(packageName)) {
+                    loadingUid = appInfo.uid;
+                } else {
+                    try {
+                        loadingUid = mPackageManager.getPackageUid(loadingPackageName, /*flags*/ 0,
+                                userId);
+                    } catch (RemoteException ignored) {
+                        // Can't happen, we're local.
+                    }
+                }
+
+                if (loadingUid != -1) {
+                    writeDclEvent(loadingUid, message);
+                }
+            }
+        }
+
+        if (needWrite) {
+            mPackageDynamicCodeLoading.maybeWriteAsync();
         }
     }
 
     @VisibleForTesting
-    /*package*/ void writeDclEvent(int uid, String message) {
+    PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+        return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
+    }
+
+    @VisibleForTesting
+    void writeDclEvent(int uid, String message) {
         EventLog.writeEvent(SNET_TAG, DCL_SUBTAG, uid, message);
     }
+
+    void record(int loaderUserId, String dexPath,
+            String owningPackageName, String loadingPackageName) {
+        if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath,
+                PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
+                loadingPackageName)) {
+            mPackageDynamicCodeLoading.maybeWriteAsync();
+        }
+    }
+
+    void clear() {
+        mPackageDynamicCodeLoading.clear();
+    }
+
+    void removePackage(String packageName) {
+        if (mPackageDynamicCodeLoading.removePackage(packageName)) {
+            mPackageDynamicCodeLoading.maybeWriteAsync();
+        }
+    }
+
+    void removeUserPackage(String packageName, int userId) {
+        if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
+            mPackageDynamicCodeLoading.maybeWriteAsync();
+        }
+    }
+
+    void readAndSync(Map<String, Set<Integer>> packageToUsersMap) {
+        mPackageDynamicCodeLoading.read();
+        mPackageDynamicCodeLoading.syncData(packageToUsersMap);
+    }
+
+    void writeNow() {
+        mPackageDynamicCodeLoading.writeNow();
+    }
 }
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index 36b7269..25ef767 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -19,7 +19,6 @@
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
 import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
-import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -90,18 +89,17 @@
     // encode and save the dex usage data.
     private final PackageDexUsage mPackageDexUsage;
 
-    // PackageDynamicCodeLoading handles recording of dynamic code loading -
-    // which is similar to PackageDexUsage but records a different aspect of the data.
+    // DexLogger handles recording of dynamic code loading - which is similar to PackageDexUsage
+    // but records a different aspect of the data.
     // (It additionally includes DEX files loaded with unsupported class loaders, and doesn't
     // record class loaders or ISAs.)
-    private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
+    private final DexLogger mDexLogger;
 
     private final IPackageManager mPackageManager;
     private final PackageDexOptimizer mPackageDexOptimizer;
     private final Object mInstallLock;
     @GuardedBy("mInstallLock")
     private final Installer mInstaller;
-    private final Listener mListener;
 
     // Possible outcomes of a dex search.
     private static int DEX_SEARCH_NOT_FOUND = 0;  // dex file not found
@@ -122,26 +120,20 @@
      */
     private final static PackageUseInfo DEFAULT_USE_INFO = new PackageUseInfo();
 
-    public interface Listener {
-        /**
-         * Invoked just before the secondary dex file {@code dexPath} for the specified application
-         * is reconciled.
-         */
-        void onReconcileSecondaryDexFile(ApplicationInfo appInfo, DexUseInfo dexUseInfo,
-                String dexPath, int storageFlags);
-    }
-
     public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo,
-            Installer installer, Object installLock, Listener listener) {
+            Installer installer, Object installLock) {
         mContext = context;
         mPackageCodeLocationsCache = new HashMap<>();
         mPackageDexUsage = new PackageDexUsage();
-        mPackageDynamicCodeLoading = new PackageDynamicCodeLoading();
         mPackageManager = pms;
         mPackageDexOptimizer = pdo;
         mInstaller = installer;
         mInstallLock = installLock;
-        mListener = listener;
+        mDexLogger = new DexLogger(pms, installer, installLock);
+    }
+
+    public DexLogger getDexLogger() {
+        return mDexLogger;
     }
 
     public void systemReady() {
@@ -243,11 +235,8 @@
                     continue;
                 }
 
-                if (mPackageDynamicCodeLoading.record(searchResult.mOwningPackageName, dexPath,
-                        PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
-                        loadingAppInfo.packageName)) {
-                    mPackageDynamicCodeLoading.maybeWriteAsync();
-                }
+                mDexLogger.record(loaderUserId, dexPath, searchResult.mOwningPackageName,
+                        loadingAppInfo.packageName);
 
                 if (classLoaderContexts != null) {
 
@@ -284,7 +273,7 @@
             loadInternal(existingPackages);
         } catch (Exception e) {
             mPackageDexUsage.clear();
-            mPackageDynamicCodeLoading.clear();
+            mDexLogger.clear();
             Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e);
         }
     }
@@ -335,16 +324,12 @@
             if (mPackageDexUsage.removePackage(packageName)) {
                 mPackageDexUsage.maybeWriteAsync();
             }
-            if (mPackageDynamicCodeLoading.removePackage(packageName)) {
-                mPackageDynamicCodeLoading.maybeWriteAsync();
-            }
+            mDexLogger.removePackage(packageName);
         } else {
             if (mPackageDexUsage.removeUserPackage(packageName, userId)) {
                 mPackageDexUsage.maybeWriteAsync();
             }
-            if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
-                mPackageDynamicCodeLoading.maybeWriteAsync();
-            }
+            mDexLogger.removeUserPackage(packageName, userId);
         }
     }
 
@@ -423,10 +408,9 @@
         }
 
         try {
-            mPackageDynamicCodeLoading.read();
-            mPackageDynamicCodeLoading.syncData(packageToUsersMap);
+            mDexLogger.readAndSync(packageToUsersMap);
         } catch (Exception e) {
-            mPackageDynamicCodeLoading.clear();
+            mDexLogger.clear();
             Slog.w(TAG, "Exception while loading package dynamic code usage. "
                     + "Starting with a fresh state.", e);
         }
@@ -460,11 +444,6 @@
         return mPackageDexUsage.getPackageUseInfo(packageName) != null;
     }
 
-    @VisibleForTesting
-    /*package*/ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
-        return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
-    }
-
     /**
      * Perform dexopt on with the given {@code options} on the secondary dex files.
      * @return true if all secondary dex files were processed successfully (compiled or skipped
@@ -574,10 +553,6 @@
                 continue;
             }
 
-            if (mListener != null) {
-                mListener.onReconcileSecondaryDexFile(info, dexUseInfo, dexPath, flags);
-            }
-
             boolean dexStillExists = true;
             synchronized(mInstallLock) {
                 try {
@@ -721,7 +696,7 @@
      */
     public void writePackageDexUsageNow() {
         mPackageDexUsage.writeNow();
-        mPackageDynamicCodeLoading.writeNow();
+        mDexLogger.writeNow();
     }
 
     private void registerSettingObserver() {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 88f645d..e1b83fc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -109,6 +109,7 @@
 import com.android.server.os.SchedulingPolicyService;
 import com.android.server.pm.BackgroundDexOptService;
 import com.android.server.pm.CrossProfileAppsService;
+import com.android.server.pm.DynamicCodeLoggingService;
 import com.android.server.pm.Installer;
 import com.android.server.pm.LauncherAppsService;
 import com.android.server.pm.OtaDexoptService;
@@ -1667,6 +1668,18 @@
             traceEnd();
 
             if (!isWatch) {
+                // We don't run this on watches as there are no plans to use the data logged
+                // on watch devices.
+                traceBeginAndSlog("StartDynamicCodeLoggingService");
+                try {
+                    DynamicCodeLoggingService.schedule(context);
+                } catch (Throwable e) {
+                    reportWtf("starting DynamicCodeLoggingService", e);
+                }
+                traceEnd();
+            }
+
+            if (!isWatch) {
                 traceBeginAndSlog("StartPruneInstantAppsJobService");
                 try {
                     PruneInstantAppsJobService.schedule(context);
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
index 87c3cd2..3b6b48b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
@@ -16,14 +16,20 @@
 
 package com.android.server.pm.dex;
 
-import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
 import android.os.storage.StorageManager;
 
 import androidx.test.filters.SmallTest;
@@ -43,13 +49,12 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.mockito.quality.Strictness;
-
-import java.util.Arrays;
+import org.mockito.stubbing.Stubber;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DexLoggerTests {
-    private static final String PACKAGE_NAME = "package.name";
+    private static final String OWNING_PACKAGE_NAME = "package.name";
     private static final String VOLUME_UUID = "volUuid";
     private static final String DEX_PATH = "/bar/foo.jar";
     private static final int STORAGE_FLAGS = StorageManager.FLAG_STORAGE_DE;
@@ -66,6 +71,7 @@
     };
     private static final String CONTENT_HASH =
             "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20";
+    private static final byte[] EMPTY_BYTES = {};
 
     @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
 
@@ -73,92 +79,191 @@
     @Mock Installer mInstaller;
     private final Object mInstallLock = new Object();
 
-    private DexManager.Listener mListener;
+    private PackageDynamicCodeLoading mPackageDynamicCodeLoading;
+    private DexLogger mDexLogger;
 
     private final ListMultimap<Integer, String> mMessagesForUid = ArrayListMultimap.create();
+    private boolean mWriteTriggered = false;
+    private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH =
+            DEX_FILENAME_HASH + " " + CONTENT_HASH;
 
     @Before
-    public void setup() {
+    public void setup() throws Exception {
+        // Disable actually attempting to do file writes.
+        mPackageDynamicCodeLoading = new PackageDynamicCodeLoading() {
+            @Override
+            void maybeWriteAsync() {
+                mWriteTriggered = true;
+            }
+
+            @Override
+            protected void writeNow(Void data) {
+                throw new AssertionError("These tests should never call this method.");
+            }
+        };
+
         // For test purposes capture log messages as well as sending to the event log.
-        mListener = new DexLogger(mPM, mInstaller, mInstallLock) {
-                @Override
+        mDexLogger = new DexLogger(mPM, mInstaller, mInstallLock, mPackageDynamicCodeLoading) {
+            @Override
                 void writeDclEvent(int uid, String message) {
                     super.writeDclEvent(uid, message);
                     mMessagesForUid.put(uid, message);
                 }
             };
+
+        // Make the owning package exist in our mock PackageManager.
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.deviceProtectedDataDir = "/bar";
+        appInfo.uid = OWNER_UID;
+        appInfo.volumeUuid = VOLUME_UUID;
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.applicationInfo = appInfo;
+
+        doReturn(packageInfo).when(mPM)
+                .getPackageInfo(OWNING_PACKAGE_NAME, /*flags*/ 0, OWNER_USER_ID);
     }
 
     @Test
-    public void testSingleAppWithFileHash() throws Exception {
-        doReturn(CONTENT_HASH_BYTES).when(mInstaller).hashSecondaryDexFile(
-            DEX_PATH, PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS);
+    public void testOneLoader_ownFile_withFileHash() throws Exception {
+        whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
 
-        runOnReconcile();
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
 
-        assertThat(mMessagesForUid.keySet()).containsExactly(OWNER_UID);
-        String expectedMessage = DEX_FILENAME_HASH + " " + CONTENT_HASH;
-        assertThat(mMessagesForUid).containsEntry(OWNER_UID, expectedMessage);
+        assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
+        assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
+
+        assertThat(mWriteTriggered).isFalse();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading())
+                .containsExactly(OWNING_PACKAGE_NAME);
     }
 
     @Test
-    public void testSingleAppNoFileHash() throws Exception {
-        doReturn(new byte[] { }).when(mInstaller).hashSecondaryDexFile(
-            DEX_PATH, PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS);
+    public void testOneLoader_ownFile_noFileHash() throws Exception {
+        whenFileIsHashed(DEX_PATH, doReturn(EMPTY_BYTES));
 
-        runOnReconcile();
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
 
-        assertThat(mMessagesForUid.keySet()).containsExactly(OWNER_UID);
+        assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
         assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+
+        // File should be removed from the DCL list, since we can't hash it.
+        assertThat(mWriteTriggered).isTrue();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty();
     }
 
     @Test
-    public void testSingleAppHashFails() throws Exception {
-        doThrow(new InstallerException("Testing failure")).when(mInstaller).hashSecondaryDexFile(
-            DEX_PATH, PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS);
+    public void testOneLoader_ownFile_hashingFails() throws Exception {
+        whenFileIsHashed(DEX_PATH, doThrow(new InstallerException("Intentional failure for test")));
 
-        runOnReconcile();
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+        assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
+        assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+
+        // File should be removed from the DCL list, since we can't hash it.
+        assertThat(mWriteTriggered).isTrue();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty();
+    }
+
+    @Test
+    public void testOneLoader_ownFile_unknownPath() {
+        recordLoad(OWNING_PACKAGE_NAME, "other/path");
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
 
         assertThat(mMessagesForUid).isEmpty();
+        assertThat(mWriteTriggered).isTrue();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty();
     }
 
     @Test
-    public void testOtherApps() throws Exception {
-        doReturn(CONTENT_HASH_BYTES).when(mInstaller).hashSecondaryDexFile(
-            DEX_PATH, PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS);
+    public void testOneLoader_differentOwner() throws Exception {
+        whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+        setPackageUid("other.package.name", 1001);
 
-        // Simulate three packages from two different UIDs
-        String packageName1 = "other1.package.name";
-        String packageName2 = "other2.package.name";
-        String packageName3 = "other3.package.name";
-        int uid1 = 1001;
-        int uid2 = 1002;
+        recordLoad("other.package.name", DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
 
-        doReturn(uid1).when(mPM).getPackageUid(packageName1, 0, OWNER_USER_ID);
-        doReturn(uid2).when(mPM).getPackageUid(packageName2, 0, OWNER_USER_ID);
-        doReturn(uid1).when(mPM).getPackageUid(packageName3, 0, OWNER_USER_ID);
-
-        runOnReconcile(packageName1, packageName2, packageName3);
-
-        assertThat(mMessagesForUid.keySet()).containsExactly(OWNER_UID, uid1, uid2);
-
-        String expectedMessage = DEX_FILENAME_HASH + " " + CONTENT_HASH;
-        assertThat(mMessagesForUid).containsEntry(OWNER_UID, expectedMessage);
-        assertThat(mMessagesForUid).containsEntry(uid1, expectedMessage);
-        assertThat(mMessagesForUid).containsEntry(uid2, expectedMessage);
+        assertThat(mMessagesForUid.keys()).containsExactly(1001);
+        assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
+        assertThat(mWriteTriggered).isFalse();
     }
 
-    private void runOnReconcile(String... otherPackageNames) {
-        ApplicationInfo appInfo = new ApplicationInfo();
-        appInfo.packageName = PACKAGE_NAME;
-        appInfo.volumeUuid = VOLUME_UUID;
-        appInfo.uid = OWNER_UID;
+    @Test
+    public void testOneLoader_differentOwner_uninstalled() throws Exception {
+        whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+        setPackageUid("other.package.name", -1);
 
-        boolean isUsedByOtherApps = otherPackageNames.length > 0;
-        DexUseInfo dexUseInfo = new DexUseInfo(
-            isUsedByOtherApps, OWNER_USER_ID, /* classLoaderContext */ null, /* loaderIsa */ null);
-        dexUseInfo.getLoadingPackages().addAll(Arrays.asList(otherPackageNames));
+        recordLoad("other.package.name", DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
 
-        mListener.onReconcileSecondaryDexFile(appInfo, dexUseInfo, DEX_PATH, STORAGE_FLAGS);
+        assertThat(mMessagesForUid).isEmpty();
+        assertThat(mWriteTriggered).isFalse();
+    }
+
+    @Test
+    public void testMultipleLoadersAndFiles() throws Exception {
+        String otherDexPath = "/bar/nosuchdir/foo.jar";
+        whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+        whenFileIsHashed(otherDexPath, doReturn(EMPTY_BYTES));
+        setPackageUid("other.package.name1", 1001);
+        setPackageUid("other.package.name2", 1002);
+
+        recordLoad("other.package.name1", DEX_PATH);
+        recordLoad("other.package.name1", otherDexPath);
+        recordLoad("other.package.name2", DEX_PATH);
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+        assertThat(mMessagesForUid.keys()).containsExactly(1001, 1001, 1002, OWNER_UID);
+        assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
+        assertThat(mMessagesForUid).containsEntry(1001, DEX_FILENAME_HASH);
+        assertThat(mMessagesForUid).containsEntry(1002, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
+        assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
+
+        assertThat(mWriteTriggered).isTrue();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading())
+                .containsExactly(OWNING_PACKAGE_NAME);
+
+        // Check the DexLogger caching is working
+        verify(mPM, atMost(1)).getPackageInfo(OWNING_PACKAGE_NAME, /*flags*/ 0, OWNER_USER_ID);
+    }
+
+    @Test
+    public void testUnknownOwner() {
+        reset(mPM);
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading("other.package.name");
+
+        assertThat(mMessagesForUid).isEmpty();
+        assertThat(mWriteTriggered).isFalse();
+        verifyZeroInteractions(mPM);
+    }
+
+    @Test
+    public void testUninstalledPackage() {
+        reset(mPM);
+        recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+        mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+        assertThat(mMessagesForUid).isEmpty();
+        assertThat(mWriteTriggered).isTrue();
+        assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty();
+    }
+
+    private void setPackageUid(String packageName, int uid) throws Exception {
+        doReturn(uid).when(mPM).getPackageUid(packageName, /*flags*/ 0, OWNER_USER_ID);
+    }
+
+    private void whenFileIsHashed(String dexPath, Stubber stubber) throws Exception {
+        stubber.when(mInstaller).hashSecondaryDexFile(
+                dexPath, OWNING_PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS);
+    }
+
+    private void recordLoad(String loadingPackageName, String dexPath) {
+        mPackageDynamicCodeLoading.record(
+                OWNING_PACKAGE_NAME, dexPath, FILE_TYPE_DEX, OWNER_USER_ID, loadingPackageName);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index fd07cb0..7cd8cedd 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -27,12 +27,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
@@ -78,7 +72,6 @@
     @Mock Installer mInstaller;
     @Mock IPackageManager mPM;
     private final Object mInstallLock = new Object();
-    @Mock DexManager.Listener mListener;
 
     private DexManager mDexManager;
 
@@ -114,9 +107,8 @@
         mBarUser0DelegateLastClassLoader = new TestData(bar, isa, mUser0,
                 DELEGATE_LAST_CLASS_LOADER_NAME);
 
-        mDexManager = new DexManager(
-            /*Context*/ null, mPM, /*PackageDexOptimizer*/ null, mInstaller, mInstallLock,
-            mListener);
+        mDexManager = new DexManager(/*Context*/ null, mPM, /*PackageDexOptimizer*/ null,
+                mInstaller, mInstallLock);
 
         // Foo and Bar are available to user0.
         // Only Bar is available to user1;
@@ -415,9 +407,10 @@
         String frameworkDex = "/system/framework/com.android.location.provider.jar";
         // Load a dex file from framework.
         notifyDexLoad(mFooUser0, Arrays.asList(frameworkDex), mUser0);
-        // The dex file should not be recognized as a package.
-        assertFalse(mDexManager.hasInfoOnPackage(frameworkDex));
-        assertNull(mDexManager.getPackageDynamicCodeInfo(frameworkDex));
+        // The dex file should not be recognized as owned by the package.
+        assertFalse(mDexManager.hasInfoOnPackage(mFooUser0.getPackageName()));
+
+        assertNull(getPackageDynamicCodeInfo(mFooUser0));
     }
 
     @Test
@@ -510,21 +503,6 @@
         assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
     }
 
-    @Test
-    public void testReconcileSecondaryDexFiles_invokesListener() throws Exception {
-        List<String> fooSecondaries = mFooUser0.getSecondaryDexPathsFromProtectedDirs();
-        notifyDexLoad(mFooUser0, fooSecondaries, mUser0);
-
-        when(mPM.getPackageInfo(mFooUser0.getPackageName(), 0, 0))
-                .thenReturn(mFooUser0.mPackageInfo);
-
-        mDexManager.reconcileSecondaryDexFiles(mFooUser0.getPackageName());
-
-        verify(mListener, times(fooSecondaries.size()))
-                .onReconcileSecondaryDexFile(any(ApplicationInfo.class),
-                        any(DexUseInfo.class), anyString(), anyInt());
-    }
-
     private void assertSecondaryUse(TestData testData, PackageUseInfo pui,
             List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId,
             String[] expectedContexts) {
@@ -585,6 +563,10 @@
         return pui;
     }
 
+    private PackageDynamicCode getPackageDynamicCodeInfo(TestData testData) {
+        return mDexManager.getDexLogger().getPackageDynamicCodeInfo(testData.getPackageName());
+    }
+
     private void assertNoUseInfo(TestData testData) {
         assertFalse(mDexManager.hasInfoOnPackage(testData.getPackageName()));
     }
@@ -600,11 +582,11 @@
     }
 
     private void assertNoDclInfo(TestData testData) {
-        assertNull(mDexManager.getPackageDynamicCodeInfo(testData.getPackageName()));
+        assertNull(getPackageDynamicCodeInfo(testData));
     }
 
     private void assertNoDclInfo(TestData testData, int userId) {
-        PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(testData.getPackageName());
+        PackageDynamicCode info = getPackageDynamicCodeInfo(testData);
         if (info == null) {
             return;
         }
@@ -615,7 +597,7 @@
     }
 
     private void assertHasDclInfo(TestData owner, TestData loader, List<String> paths) {
-        PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(owner.getPackageName());
+        PackageDynamicCode info = getPackageDynamicCodeInfo(owner);
         assertNotNull("No DCL data for owner " + owner.getPackageName(), info);
         for (String path : paths) {
             DynamicCodeFile fileInfo = info.mFileUsageMap.get(path);
diff --git a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
index d8b3b20..75ee089 100644
--- a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
+++ b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
@@ -18,20 +18,23 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.UiAutomation;
 import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.LargeTest;
 import android.util.EventLog;
+
 import dalvik.system.DexClassLoader;
 
-import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.InputStream;
@@ -40,6 +43,7 @@
 import java.util.ArrayList;
 import java.util.Formatter;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Integration tests for {@link com.android.server.pm.dex.DexLogger}.
@@ -47,10 +51,10 @@
  * The setup for the test dynamically loads code in a jar extracted
  * from our assets (a secondary dex file).
  *
- * We then use adb to trigger secondary dex file reconcilation (and
- * wait for it to complete). As a side-effect of this DexLogger should
- * be notified of the file and should log the hash of the file's name
- * and content.  We verify that this message appears in the event log.
+ * We then use shell commands to trigger dynamic code logging (and wait
+ * for it to complete). This causes DexLogger to log the hash of the
+ * file's name and content.  We verify that this message appears in
+ * the event log.
  *
  * Run with "atest DexLoggerIntegrationTests".
  */
@@ -58,29 +62,89 @@
 @RunWith(JUnit4.class)
 public final class DexLoggerIntegrationTests {
 
-    private static final String PACKAGE_NAME = "com.android.frameworks.dexloggertest";
-
     // Event log tag used for SNET related events
     private static final int SNET_TAG = 0x534e4554;
+
     // Subtag used to distinguish dynamic code loading events
     private static final String DCL_SUBTAG = "dcl";
 
-    // Obtained via "echo -n copied.jar | sha256sum"
-    private static final String EXPECTED_NAME_HASH =
-            "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C";
+    // All the tags we care about
+    private static final int[] TAG_LIST = new int[] { SNET_TAG };
 
-    private static String expectedContentHash;
+    // This is {@code DynamicCodeLoggingService#JOB_ID}
+    private static final int DYNAMIC_CODE_LOGGING_JOB_ID = 2030028;
+
+    private static Context sContext;
+    private static int sMyUid;
 
     @BeforeClass
-    public static void setUpAll() throws Exception {
-        Context context = InstrumentationRegistry.getTargetContext();
+    public static void setUpAll() {
+        sContext = InstrumentationRegistry.getTargetContext();
+        sMyUid = android.os.Process.myUid();
+    }
+
+    @Before
+    public void primeEventLog() {
+        // Force a round trip to logd to make sure everything is up to date.
+        // Without this the first test passes and others don't - we don't see new events in the
+        // log. The exact reason is unclear.
+        EventLog.writeEvent(SNET_TAG, "Dummy event");
+    }
+
+    @Test
+    public void testDexLoggerGeneratesEvents() throws Exception {
+        File privateCopyFile = fileForJar("copied.jar");
+        // Obtained via "echo -n copied.jar | sha256sum"
+        String expectedNameHash =
+                "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C";
+        String expectedContentHash = copyAndHashJar(privateCopyFile);
+
+        // Feed the jar to a class loader and make sure it contains what we expect.
+        ClassLoader parentClassLoader = sContext.getClass().getClassLoader();
+        ClassLoader loader =
+                new DexClassLoader(privateCopyFile.toString(), null, null, parentClassLoader);
+        loader.loadClass("com.android.dcl.Simple");
+
+        // And make sure we log events about it
+        long previousEventNanos = mostRecentEventTimeNanos();
+        runDexLogger();
+
+        assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+    }
+
+    @Test
+
+    public void testDexLoggerGeneratesEvents_unknownClassLoader() throws Exception {
+        File privateCopyFile = fileForJar("copied2.jar");
+        String expectedNameHash =
+                "202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93";
+        String expectedContentHash = copyAndHashJar(privateCopyFile);
+
+        // This time make sure an unknown class loader is an ancestor of the class loader we use.
+        ClassLoader knownClassLoader = sContext.getClass().getClassLoader();
+        ClassLoader unknownClassLoader = new UnknownClassLoader(knownClassLoader);
+        ClassLoader loader =
+                new DexClassLoader(privateCopyFile.toString(), null, null, unknownClassLoader);
+        loader.loadClass("com.android.dcl.Simple");
+
+        // And make sure we log events about it
+        long previousEventNanos = mostRecentEventTimeNanos();
+        runDexLogger();
+
+        assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+    }
+
+    private static File fileForJar(String name) {
+        return new File(sContext.getDir("jars", Context.MODE_PRIVATE), name);
+    }
+
+    private static String copyAndHashJar(File copyTo) throws Exception {
         MessageDigest hasher = MessageDigest.getInstance("SHA-256");
 
         // Copy the jar from our Java resources to a private data directory
-        File privateCopy = new File(context.getDir("jars", Context.MODE_PRIVATE), "copied.jar");
         Class<?> thisClass = DexLoggerIntegrationTests.class;
         try (InputStream input = thisClass.getResourceAsStream("/javalib.jar");
-                OutputStream output = new FileOutputStream(privateCopy)) {
+                OutputStream output = new FileOutputStream(copyTo)) {
             byte[] buffer = new byte[1024];
             while (true) {
                 int numRead = input.read(buffer);
@@ -92,42 +156,63 @@
             }
         }
 
-        // Remember the SHA-256 of the file content to check that it is the same as
-        // the value we see logged.
+        // Compute the SHA-256 of the file content so we can check that it is the same as the value
+        // we see logged.
         Formatter formatter = new Formatter();
         for (byte b : hasher.digest()) {
             formatter.format("%02X", b);
         }
-        expectedContentHash = formatter.toString();
 
-        // Feed the jar to a class loader and make sure it contains what we expect.
-        ClassLoader loader =
-                new DexClassLoader(
-                    privateCopy.toString(), null, null, context.getClass().getClassLoader());
-        loader.loadClass("com.android.dcl.Simple");
+        return formatter.toString();
     }
 
-    @Test
-    public void testDexLoggerReconcileGeneratesEvents() throws Exception {
-        int[] tagList = new int[] { SNET_TAG };
+    private static long mostRecentEventTimeNanos() throws Exception {
         List<EventLog.Event> events = new ArrayList<>();
 
-        // There may already be events in the event log - figure out the most recent one
-        EventLog.readEvents(tagList, events);
-        long previousEventNanos =
-                events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
-        events.clear();
+        EventLog.readEvents(TAG_LIST, events);
+        return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
+    }
 
-        Process process = Runtime.getRuntime().exec(
-            "cmd package reconcile-secondary-dex-files " + PACKAGE_NAME);
-        int exitCode = process.waitFor();
-        assertThat(exitCode).isEqualTo(0);
+    private static void runDexLogger() throws Exception {
+        // This forces {@code DynamicCodeLoggingService} to start now.
+        runCommand("cmd jobscheduler run -f android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+        // Wait for the job to have run.
+        long startTime = SystemClock.elapsedRealtime();
+        while (true) {
+            String response = runCommand(
+                    "cmd jobscheduler get-job-state android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+            if (!response.contains("pending") && !response.contains("active")) {
+                break;
+            }
+            if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) {
+                throw new AssertionError("Job has not completed: " + response);
+            }
+            SystemClock.sleep(100);
+        }
+    }
 
-        int myUid = android.os.Process.myUid();
-        String expectedMessage = EXPECTED_NAME_HASH + " " + expectedContentHash;
+    private static String runCommand(String command) throws Exception {
+        ByteArrayOutputStream response = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1000];
+        UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        ParcelFileDescriptor fd = ui.executeShellCommand(command);
+        try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
+            while (true) {
+                int count = input.read(buffer);
+                if (count == -1) {
+                    break;
+                }
+                response.write(buffer, 0, count);
+            }
+        }
+        return response.toString("UTF-8");
+    }
 
-        EventLog.readEvents(tagList, events);
-        boolean found = false;
+    private static void assertDclLoggedSince(long previousEventNanos, String expectedNameHash,
+            String expectedContentHash) throws Exception {
+        List<EventLog.Event> events = new ArrayList<>();
+        EventLog.readEvents(TAG_LIST, events);
+        int found = 0;
         for (EventLog.Event event : events) {
             if (event.getTimeNanos() <= previousEventNanos) {
                 continue;
@@ -140,15 +225,28 @@
                 continue;
             }
             int uid = (int) data[1];
-            if (uid != myUid) {
+            if (uid != sMyUid) {
                 continue;
             }
 
             String message = (String) data[2];
-            assertThat(message).isEqualTo(expectedMessage);
-            found = true;
+            if (!message.startsWith(expectedNameHash)) {
+                continue;
+            }
+
+            assertThat(message).endsWith(expectedContentHash);
+            ++found;
         }
 
-        assertThat(found).isTrue();
+        assertThat(found).isEqualTo(1);
+    }
+
+    /**
+     * A class loader that does nothing useful, but importantly doesn't extend BaseDexClassLoader.
+     */
+    private static class UnknownClassLoader extends ClassLoader {
+        UnknownClassLoader(ClassLoader parent) {
+            super(parent);
+        }
     }
 }