Persist dex use information.

After this change, dex use information can be saved in a proto.

Also, this CL enables Proguard in order to reduce the cost of
introducing protobuf.

Bug: 249984283
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: Ifae646e12241364d5edb3cc5ecd4c8548539b9c4
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 5c3ca8e..f11d902 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -87,12 +87,36 @@
     static_libs: [
         "artd-aidl-java",
         "modules-utils-shell-command-handler",
+        "service-art-proto-java",
     ],
     plugins: [
         "auto_value_plugin",
         "java_api_finder",
     ],
     jarjar_rules: "jarjar-rules.txt",
+    optimize: {
+        enabled: true,
+        optimize: true,
+        shrink: true,
+        proguard_compatibility: false,
+        proguard_flags_files: ["proguard.flags"],
+    },
+}
+
+java_library {
+    name: "service-art-proto-java",
+    proto: {
+        type: "lite",
+    },
+    srcs: [
+        "proto/**/*.proto",
+    ],
+    sdk_version: "system_server_current",
+    min_sdk_version: "31",
+    apex_available: [
+        "com.android.art",
+        "com.android.art.debug",
+    ],
 }
 
 art_cc_defaults {
diff --git a/libartservice/service/jarjar-rules.txt b/libartservice/service/jarjar-rules.txt
index c7d39e6..54ff0a1 100644
--- a/libartservice/service/jarjar-rules.txt
+++ b/libartservice/service/jarjar-rules.txt
@@ -1,2 +1,3 @@
 # Repackages static libraries to make them private to ART Services.
 rule com.android.modules.utils.** com.android.server.art.jarjar.@0
+rule com.google.protobuf.** com.android.server.art.jarjar.@0
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index e61b294..b9485a0 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -170,6 +170,26 @@
                     }
                     return 0;
                 }
+                case "dex-use-dump": {
+                    pw.println(mDexUseManager.dump());
+                    return 0;
+                }
+                case "dex-use-save": {
+                    try {
+                        mDexUseManager.save(getNextArgRequired());
+                        return 0;
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+                case "dex-use-load": {
+                    try {
+                        mDexUseManager.load(getNextArgRequired());
+                        return 0;
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                }
                 default:
                     // Handles empty, help, and invalid commands.
                     return handleDefaultCommands(cmd);
@@ -217,6 +237,12 @@
         pw.println("  dex-use-get-secondary PACKAGE_NAME");
         pw.println("    Print the dex use information about all secondary dex files owned by the");
         pw.println("    given package.");
+        pw.println("  dex-use-dump");
+        pw.println("    Print all dex use information in textproto format.");
+        pw.println("  dex-use-save PATH");
+        pw.println("    Save dex use information to a file in binary proto format.");
+        pw.println("  dex-use-load PATH");
+        pw.println("    Load dex use information from a file in binary proto format.");
     }
 
     private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
index 790cd86..5c023e1 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManager.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -25,6 +25,13 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.Immutable;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.proto.DexUseProto;
+import com.android.server.art.proto.Int32Value;
+import com.android.server.art.proto.PackageDexUseProto;
+import com.android.server.art.proto.PrimaryDexUseProto;
+import com.android.server.art.proto.PrimaryDexUseRecordProto;
+import com.android.server.art.proto.SecondaryDexUseProto;
+import com.android.server.art.proto.SecondaryDexUseRecordProto;
 import com.android.server.art.wrapper.Environment;
 import com.android.server.art.wrapper.Process;
 import com.android.server.pm.PackageManagerLocal;
@@ -271,6 +278,35 @@
         mDexUse = new DexUse();
     }
 
+    public @NonNull String dump() {
+        var builder = DexUseProto.newBuilder();
+        synchronized (this) {
+            mDexUse.toProto(builder);
+        }
+        return builder.build().toString();
+    }
+
+    public void save(@NonNull String filename) throws IOException {
+        try (OutputStream out = new FileOutputStream(filename)) {
+            var builder = DexUseProto.newBuilder();
+            synchronized (this) {
+                mDexUse.toProto(builder);
+            }
+            builder.build().writeTo(out);
+        }
+    }
+
+    public void load(@NonNull String filename) throws IOException {
+        try (InputStream in = new FileInputStream(filename)) {
+            var proto = DexUseProto.parseFrom(in);
+            var dexUse = new DexUse();
+            dexUse.fromProto(proto);
+            synchronized (this) {
+                mDexUse = dexUse;
+            }
+        }
+    }
+
     private static boolean isUsedByOtherApps(
             @NonNull Set<DexLoader> loaders, @NonNull String owningPackageName) {
         // If the dex file is loaded by an isolated process of the same app, it can also be
@@ -364,6 +400,24 @@
 
     private static class DexUse {
         @NonNull Map<String, PackageDexUse> mPackageDexUseByOwningPackageName = new HashMap<>();
+
+        void toProto(@NonNull DexUseProto.Builder builder) {
+            for (var entry : mPackageDexUseByOwningPackageName.entrySet()) {
+                var packageBuilder =
+                        PackageDexUseProto.newBuilder().setOwningPackageName(entry.getKey());
+                entry.getValue().toProto(packageBuilder);
+                builder.addPackageDexUse(packageBuilder);
+            }
+        }
+
+        void fromProto(@NonNull DexUseProto proto) {
+            for (PackageDexUseProto packageProto : proto.getPackageDexUseList()) {
+                var packageDexUse = new PackageDexUse();
+                packageDexUse.fromProto(packageProto);
+                mPackageDexUseByOwningPackageName.put(
+                        Utils.assertNonEmpty(packageProto.getOwningPackageName()), packageDexUse);
+            }
+        }
     }
 
     private static class PackageDexUse {
@@ -378,15 +432,84 @@
          * JARs in CE and DE directories).
          */
         @NonNull Map<String, SecondaryDexUse> mSecondaryDexUseByDexFile = new HashMap<>();
+
+        void toProto(@NonNull PackageDexUseProto.Builder builder) {
+            for (var entry : mPrimaryDexUseByDexFile.entrySet()) {
+                var primaryBuilder = PrimaryDexUseProto.newBuilder().setDexFile(entry.getKey());
+                entry.getValue().toProto(primaryBuilder);
+                builder.addPrimaryDexUse(primaryBuilder);
+            }
+            for (var entry : mSecondaryDexUseByDexFile.entrySet()) {
+                var secondaryBuilder = SecondaryDexUseProto.newBuilder().setDexFile(entry.getKey());
+                entry.getValue().toProto(secondaryBuilder);
+                builder.addSecondaryDexUse(secondaryBuilder);
+            }
+        }
+
+        void fromProto(@NonNull PackageDexUseProto proto) {
+            for (PrimaryDexUseProto primaryProto : proto.getPrimaryDexUseList()) {
+                var primaryDexUse = new PrimaryDexUse();
+                primaryDexUse.fromProto(primaryProto);
+                mPrimaryDexUseByDexFile.put(
+                        Utils.assertNonEmpty(primaryProto.getDexFile()), primaryDexUse);
+            }
+            for (SecondaryDexUseProto secondaryProto : proto.getSecondaryDexUseList()) {
+                var secondaryDexUse = new SecondaryDexUse();
+                secondaryDexUse.fromProto(secondaryProto);
+                mSecondaryDexUseByDexFile.put(
+                        Utils.assertNonEmpty(secondaryProto.getDexFile()), secondaryDexUse);
+            }
+        }
     }
 
     private static class PrimaryDexUse {
         @NonNull Set<DexLoader> mLoaders = new HashSet<>();
+
+        void toProto(@NonNull PrimaryDexUseProto.Builder builder) {
+            for (DexLoader loader : mLoaders) {
+                builder.addRecord(PrimaryDexUseRecordProto.newBuilder()
+                                          .setLoadingPackageName(loader.loadingPackageName())
+                                          .setIsolatedProcess(loader.isolatedProcess()));
+            }
+        }
+
+        void fromProto(@NonNull PrimaryDexUseProto proto) {
+            for (PrimaryDexUseRecordProto recordProto : proto.getRecordList()) {
+                mLoaders.add(
+                        DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
+                                recordProto.getIsolatedProcess()));
+            }
+        }
     }
 
     private static class SecondaryDexUse {
         @Nullable UserHandle mUserHandle = null;
         @NonNull Map<DexLoader, SecondaryDexUseRecord> mRecordByLoader = new HashMap<>();
+
+        void toProto(@NonNull SecondaryDexUseProto.Builder builder) {
+            builder.setUserId(Int32Value.newBuilder().setValue(mUserHandle.getIdentifier()));
+            for (var entry : mRecordByLoader.entrySet()) {
+                var recordBuilder =
+                        SecondaryDexUseRecordProto.newBuilder()
+                                .setLoadingPackageName(entry.getKey().loadingPackageName())
+                                .setIsolatedProcess(entry.getKey().isolatedProcess());
+                entry.getValue().toProto(recordBuilder);
+                builder.addRecord(recordBuilder);
+            }
+        }
+
+        void fromProto(@NonNull SecondaryDexUseProto proto) {
+            Utils.check(proto.hasUserId());
+            mUserHandle = UserHandle.of(proto.getUserId().getValue());
+            for (SecondaryDexUseRecordProto recordProto : proto.getRecordList()) {
+                var record = new SecondaryDexUseRecord();
+                record.fromProto(recordProto);
+                mRecordByLoader.put(
+                        DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
+                                recordProto.getIsolatedProcess()),
+                        record);
+            }
+        }
     }
 
     /** Represents an entity that loads a dex file. */
@@ -409,5 +532,14 @@
         // reported by the app.
         @Nullable String mClassLoaderContext = null;
         @Nullable String mAbiName = null;
+
+        void toProto(@NonNull SecondaryDexUseRecordProto.Builder builder) {
+            builder.setClassLoaderContext(mClassLoaderContext).setAbiName(mAbiName);
+        }
+
+        void fromProto(@NonNull SecondaryDexUseRecordProto proto) {
+            mClassLoaderContext = Utils.assertNonEmpty(proto.getClassLoaderContext());
+            mAbiName = Utils.assertNonEmpty(proto.getAbiName());
+        }
     }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index 6c66190..aebc502 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -187,7 +187,17 @@
 
     /** Checks that it ignores and dedups things correctly. */
     @Test
-    public void testPrimaryDexMultipleEntries() {
+    public void testPrimaryDexMultipleEntries() throws Exception {
+        verifyPrimaryDexMultipleEntries(false /* saveAndLoad */);
+    }
+
+    /** Checks that it saves and loads data correctly. */
+    @Test
+    public void testPrimaryDexMultipleEntriesPersisted() throws Exception {
+        verifyPrimaryDexMultipleEntries(true /*saveAndLoad */);
+    }
+
+    private void verifyPrimaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
         // These should be ignored.
         mDexUseManager.addDexUse(mSnapshot, "android", Map.of(BASE_APK, "CLC"));
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
@@ -206,6 +216,13 @@
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
 
+        if (saveAndLoad) {
+            File tempFile = File.createTempFile("dex-use", ".pb");
+            mDexUseManager.save(tempFile.getPath());
+            mDexUseManager.clear();
+            mDexUseManager.load(tempFile.getPath());
+        }
+
         assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
                 .containsExactly(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
                         DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */),
@@ -287,7 +304,17 @@
 
     /** Checks that it ignores and dedups things correctly. */
     @Test
-    public void testSecondaryDexMultipleEntries() {
+    public void testSecondaryDexMultipleEntries() throws Exception {
+        verifySecondaryDexMultipleEntries(false /*saveAndLoad */);
+    }
+
+    /** Checks that it saves and loads data correctly. */
+    @Test
+    public void testSecondaryDexMultipleEntriesPersisted() throws Exception {
+        verifySecondaryDexMultipleEntries(true /*saveAndLoad */);
+    }
+
+    private void verifySecondaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
         // These should be ignored.
         mDexUseManager.addDexUse(mSnapshot, "android", Map.of(mCeDir + "/foo.apk", "CLC"));
         mDexUseManager.addDexUse(
@@ -316,6 +343,13 @@
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
                 Map.of(mCeDir + "/foo.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
 
+        if (saveAndLoad) {
+            File tempFile = File.createTempFile("dex-use", ".pb");
+            mDexUseManager.save(tempFile.getPath());
+            mDexUseManager.clear();
+            mDexUseManager.load(tempFile.getPath());
+        }
+
         List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
                 .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
diff --git a/libartservice/service/proguard.flags b/libartservice/service/proguard.flags
new file mode 100644
index 0000000..3589496
--- /dev/null
+++ b/libartservice/service/proguard.flags
@@ -0,0 +1,12 @@
+# Only keep APIs.
+-keep @android.annotation.SystemApi class * {
+  public *;
+}
+
+# Proto field names are used by MessageLiteToString.toString through reflection.
+-keepclassmembers class * extends
+    com.android.server.art.jarjar.com.google.protobuf.GeneratedMessageLite {
+  *** get*();
+  *** set*(***);
+  *** has*();
+}
diff --git a/libartservice/service/proto/common.proto b/libartservice/service/proto/common.proto
new file mode 100644
index 0000000..c8bb565
--- /dev/null
+++ b/libartservice/service/proto/common.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.art.proto;
+option java_multiple_files = true;
+
+// Wrapper message for `int32`, to distinguish between the absence of a field
+// and its default value.
+message Int32Value {
+    int32 value = 1;
+}
diff --git a/libartservice/service/proto/dex_use.proto b/libartservice/service/proto/dex_use.proto
new file mode 100644
index 0000000..53e5107
--- /dev/null
+++ b/libartservice/service/proto/dex_use.proto
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.art.proto;
+option java_multiple_files = true;
+
+import "art/libartservice/service/proto/common.proto";
+
+// The protobuf representation of `DexUseManager.DexUse`. See classes in
+// java/com/android/server/art/DexUseManager.java for details.
+// This proto is persisted on disk and both forward and backward compatibility are considerations.
+message DexUseProto {
+    repeated PackageDexUseProto package_dex_use = 1;
+}
+
+message PackageDexUseProto {
+    string owning_package_name = 1;
+    repeated PrimaryDexUseProto primary_dex_use = 2;
+    repeated SecondaryDexUseProto secondary_dex_use = 3;
+}
+
+message PrimaryDexUseProto {
+    string dex_file = 1;
+    repeated PrimaryDexUseRecordProto record = 2;
+}
+
+message PrimaryDexUseRecordProto {
+    string loading_package_name = 1;
+    bool isolated_process = 2;
+}
+
+message SecondaryDexUseProto {
+    string dex_file = 1;
+    Int32Value user_id = 2;  // Must be explicitly set.
+    repeated SecondaryDexUseRecordProto record = 3;
+}
+
+message SecondaryDexUseRecordProto {
+    string loading_package_name = 1;
+    bool isolated_process = 2;
+    string class_loader_context = 3;
+    string abi_name = 4;
+}