diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ff73df6cb876a..d9e6240679ea1 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4685,6 +4685,10 @@ android:permission="android.permission.BIND_JOB_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 0000000000000..2ae424dd4b1b5 --- /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 edab94c963f14..9aebee0a41767 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.Installer.InstallerException; 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub /** * 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 88d9e52ccf511..68a755b382cad 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 @@ package com.android.server.pm.dex; 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 @@ public class DexLogger implements DexManager.Listener { 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 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 appInfoByUser = new SparseArray<>(); + boolean needWrite = false; - writeDclEvent(ownerUid, message); + for (Map.Entry fileEntry : info.mFileUsageMap.entrySet()) { + String filePath = fileEntry.getKey(); + DynamicCodeFile fileInfo = fileEntry.getValue(); + int userId = fileInfo.mUserId; + + int index = appInfoByUser.indexOfKey(userId); + ApplicationInfo appInfo; + if (index >= 0) { + appInfo = appInfoByUser.get(userId); + } else { + appInfo = null; - if (dexUseInfo.isUsedByOtherApps()) { - Set otherPackages = dexUseInfo.getLoadingPackages(); - Set otherUids = new ArraySet<>(otherPackages.size()); - for (String otherPackageName : otherPackages) { 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> 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 36b7269576b65..25ef7675e2b9f 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 @@ package com.android.server.pm.dex; 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 @@ public class DexManager { // 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 @@ public class DexManager { */ 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 @@ public class DexManager { 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 @@ public class DexManager { 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 @@ public class DexManager { 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 @@ public class DexManager { } 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 @@ public class DexManager { 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 @@ public class DexManager { continue; } - if (mListener != null) { - mListener.onReconcileSecondaryDexFile(info, dexUseInfo, dexPath, flags); - } - boolean dexStillExists = true; synchronized(mInstallLock) { try { @@ -721,7 +696,7 @@ public class DexManager { */ 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 88f645defa6da..e1b83fc998973 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.DeviceIdentifiersPolicyService; 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; @@ -1666,6 +1667,18 @@ public final class SystemServer { } 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 { 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 87c3cd2dad065..3b6b48b6aa3fd 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.Mock; 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 @@ public class DexLoggerTests { }; 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 @@ public class DexLoggerTests { @Mock Installer mInstaller; private final Object mInstallLock = new Object(); - private DexManager.Listener mListener; + private PackageDynamicCodeLoading mPackageDynamicCodeLoading; + private DexLogger mDexLogger; private final ListMultimap 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 fd07cb046fb5b..7cd8ceddfd23f 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.assertNotEquals; 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 @@ public class DexManagerTests { @Mock Installer mInstaller; @Mock IPackageManager mPM; private final Object mInstallLock = new Object(); - @Mock DexManager.Listener mListener; private DexManager mDexManager; @@ -114,9 +107,8 @@ public class DexManagerTests { 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 @@ public class DexManagerTests { 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 @@ public class DexManagerTests { assertHasDclInfo(mBarUser0, mBarUser0, secondaries); } - @Test - public void testReconcileSecondaryDexFiles_invokesListener() throws Exception { - List 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 secondaries, boolean isUsedByOtherApps, int ownerUserId, String[] expectedContexts) { @@ -585,6 +563,10 @@ public class DexManagerTests { 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 @@ public class DexManagerTests { } 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 @@ public class DexManagerTests { } private void assertHasDclInfo(TestData owner, TestData loader, List 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 d8b3b20863354..75ee0896c23a0 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 @@ package com.android.server.pm.dex; 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.security.MessageDigest; 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 @@ import java.util.List; * 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 @@ import java.util.List; @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 @@ public final class DexLoggerIntegrationTests { } } - // 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 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 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 @@ public final class DexLoggerIntegrationTests { 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); + } } }