Add test for font crash protection.

This test modifies the installed font file with block_device_writer.
FontManagerService should detect it and remove the modified file.

Bug: 176939176
Test: atest ApkVerityTest
Test: atest UpdatableSystemFontTest
Change-Id: I7da3f2911459619d5d56a94e091b912d67cb27d3
diff --git a/tests/UpdatableSystemFontTest/Android.bp b/tests/UpdatableSystemFontTest/Android.bp
index d809fe8..43a5078 100644
--- a/tests/UpdatableSystemFontTest/Android.bp
+++ b/tests/UpdatableSystemFontTest/Android.bp
@@ -16,8 +16,14 @@
     name: "UpdatableSystemFontTest",
     srcs: ["src/**/*.java"],
     libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"],
-    static_libs: ["frameworks-base-hostutils"],
+    static_libs: [
+        "block_device_writer_jar",
+        "frameworks-base-hostutils",
+    ],
     test_suites: ["general-tests", "vts"],
+    target_required: [
+        "block_device_writer_module",
+    ],
     data: [
         ":NotoColorEmojiTtf",
         ":UpdatableSystemFontTestCertDer",
diff --git a/tests/UpdatableSystemFontTest/AndroidTest.xml b/tests/UpdatableSystemFontTest/AndroidTest.xml
index efe5d70..7b919bd 100644
--- a/tests/UpdatableSystemFontTest/AndroidTest.xml
+++ b/tests/UpdatableSystemFontTest/AndroidTest.xml
@@ -21,6 +21,7 @@
 
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
+        <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" />
         <option name="push" value="UpdatableSystemFontTestCert.der->/data/local/tmp/UpdatableSystemFontTestCert.der" />
         <option name="push" value="NotoColorEmoji.ttf->/data/local/tmp/NotoColorEmoji.ttf" />
         <option name="push" value="UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig" />
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index 6d161a5..e249f8a9 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -21,7 +21,9 @@
 
 import android.platform.test.annotations.RootPermissionTest;
 
+import com.android.blockdevicewriter.BlockDeviceWriter;
 import com.android.fsverity.AddFsVerityCertRule;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -34,6 +36,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -126,6 +130,44 @@
                 TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
     }
 
+    @Test
+    public void reboot() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath).startsWith("/data/fonts/files/");
+
+        expectRemoteCommandToSucceed("stop");
+        expectRemoteCommandToSucceed("start");
+        waitUntilFontCommandIsReady();
+        String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPathAfterReboot).isEqualTo(fontPath);
+    }
+
+    @Test
+    public void reboot_clearDamagedFiles() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath).startsWith("/data/fonts/files/");
+        assertThat(BlockDeviceWriter.canReadByte(getDevice(), fontPath, 0)).isTrue();
+
+        BlockDeviceWriter.damageFileAgainstBlockDevice(getDevice(), fontPath, 0);
+        expectRemoteCommandToSucceed("stop");
+        // We have to make sure system_server is gone before dropping caches, because system_server
+        // process holds font memory maps and prevents cache eviction.
+        waitUntilSystemServerIsGone();
+        BlockDeviceWriter.assertFileNotOpen(getDevice(), fontPath);
+        BlockDeviceWriter.dropCaches(getDevice());
+        assertThat(BlockDeviceWriter.canReadByte(getDevice(), fontPath, 0)).isFalse();
+
+        expectRemoteCommandToSucceed("start");
+        waitUntilFontCommandIsReady();
+        String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertWithMessage("Damaged file should be deleted")
+                .that(fontPathAfterReboot).startsWith("/system");
+    }
+
     private String getFontPath(String fontFileName) throws Exception {
         // TODO: add a dedicated command for testing.
         String lines = expectRemoteCommandToSucceed("cmd font dump");
@@ -153,4 +195,39 @@
                 .that(result.getStatus())
                 .isNotEqualTo(CommandStatus.SUCCESS);
     }
+
+    private void waitUntilFontCommandIsReady() {
+        waitUntil(TimeUnit.SECONDS.toMillis(30), () -> {
+            try {
+                return getDevice().executeShellV2Command("cmd font status").getStatus()
+                        == CommandStatus.SUCCESS;
+            } catch (DeviceNotAvailableException e) {
+                return false;
+            }
+        });
+    }
+
+    private void waitUntilSystemServerIsGone() {
+        waitUntil(TimeUnit.SECONDS.toMillis(30), () -> {
+            try {
+                return getDevice().executeShellV2Command("pid system_server").getStatus()
+                        == CommandStatus.FAILED;
+            } catch (DeviceNotAvailableException e) {
+                return false;
+            }
+        });
+    }
+
+    private void waitUntil(long timeoutMillis, Supplier<Boolean> func) {
+        long untilMillis = System.currentTimeMillis() + timeoutMillis;
+        do {
+            if (func.get()) return;
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                throw new AssertionError("Interrupted", e);
+            }
+        } while (System.currentTimeMillis() < untilMillis);
+        throw new AssertionError("Timed out");
+    }
 }