Make UpdatableSystemFontTest device side test.

As commit 679a824773aee8c4424787e9f36d10de1f984112 removed 'adb shell
stop', this test doesn't need to be a host side test.

This allows us to call FontManager Java API in UpdatableSystemFontTest.
I will rewrite 'cmd font' to FontManager API calls in a following CL.

Bug: 186966067
Test: atest UpdatableSystemFontTest
Change-Id: I3bad29a3ea8402c990ae0dd553d3230db2d9f67c
diff --git a/tests/UpdatableSystemFontTest/Android.bp b/tests/UpdatableSystemFontTest/Android.bp
index 8b0ae5c..ea5a431 100644
--- a/tests/UpdatableSystemFontTest/Android.bp
+++ b/tests/UpdatableSystemFontTest/Android.bp
@@ -21,16 +21,15 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-java_test_host {
+android_test {
     name: "UpdatableSystemFontTest",
     srcs: ["src/**/*.java"],
-    libs: [
-        "tradefed",
-        "compatibility-tradefed",
-        "compatibility-host-util",
-    ],
+    libs: ["android.test.runner"],
     static_libs: [
-        "frameworks-base-hostutils",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "platform-test-annotations",
+        "truth-prebuilt",
     ],
     test_suites: [
         "general-tests",
@@ -47,4 +46,5 @@
         ":UpdatableSystemFontTestNotoColorEmojiVPlus2Ttf",
         ":UpdatableSystemFontTestNotoColorEmojiVPlus2TtfFsvSig",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/UpdatableSystemFontTest/AndroidManifest.xml b/tests/UpdatableSystemFontTest/AndroidManifest.xml
new file mode 100644
index 0000000..531ee98
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2021 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.updatablesystemfont">
+
+    <application android:label="UpdatableSystemFontTest">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="UpdatableSystemFontTest"
+         android:targetPackage="com.android.updatablesystemfont">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/UpdatableSystemFontTest/AndroidTest.xml b/tests/UpdatableSystemFontTest/AndroidTest.xml
index 4f11669..4f6487e 100644
--- a/tests/UpdatableSystemFontTest/AndroidTest.xml
+++ b/tests/UpdatableSystemFontTest/AndroidTest.xml
@@ -21,6 +21,7 @@
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="UpdatableSystemFontTest.apk" />
         <option name="test-file-name" value="EmojiRenderingTestApp.apk" />
     </target_preparer>
 
@@ -37,7 +38,7 @@
         <option name="push" value="UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig" />
     </target_preparer>
 
-    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
-        <option name="jar" value="UpdatableSystemFontTest.jar" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.updatablesystemfont" />
     </test>
 </configuration>
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index 74f6bca..79e23b8 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -17,26 +17,35 @@
 package com.android.updatablesystemfont;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.RootPermissionTest;
+import android.security.FileIntegrityManager;
+import android.util.Log;
+import android.util.Pair;
 
-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;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.StreamUtil;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -44,9 +53,10 @@
  * Tests if fonts can be updated by 'cmd font'.
  */
 @RootPermissionTest
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
+@RunWith(AndroidJUnit4.class)
+public class UpdatableSystemFontTest {
 
+    private static final String TAG = "UpdatableSystemFontTest";
     private static final String SYSTEM_FONTS_DIR = "/system/fonts/";
     private static final String DATA_FONTS_DIR = "/data/fonts/files/";
 
@@ -84,58 +94,65 @@
         T get() throws Exception;
     }
 
-    @Rule
-    public final AddFsVerityCertRule mAddFsverityCertRule =
-            new AddFsVerityCertRule(this, CERT_PATH);
+    private String mKeyId;
 
     @Before
     public void setUp() throws Exception {
-        expectRemoteCommandToSucceed("cmd font clear");
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        // Run tests only if updatable system font is enabled.
+        FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class);
+        assumeTrue(fim != null);
+        assumeTrue(fim.isApkVeritySupported());
+        mKeyId = insertCert(CERT_PATH);
+        expectCommandToSucceed("cmd font clear");
     }
 
     @After
     public void tearDown() throws Exception {
-        expectRemoteCommandToSucceed("cmd font clear");
+        expectCommandToSucceed("cmd font clear");
+        if (mKeyId != null) {
+            expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
+        }
     }
 
     @Test
     public void updateFont() throws Exception {
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(fontPath).startsWith(DATA_FONTS_DIR);
         // The updated font should be readable and unmodifiable.
-        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
-        expectRemoteCommandToFail("echo -n '' >> " + fontPath);
+        expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null");
+        expectCommandToFail("dd status=none if=" + CERT_PATH + " of=" + fontPath);
     }
 
     @Test
     public void updateFont_twice() throws Exception {
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
         String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(fontPath2).startsWith(DATA_FONTS_DIR);
         assertThat(fontPath2).isNotEqualTo(fontPath);
         // The new file should be readable.
-        expectRemoteCommandToSucceed("cat " + fontPath2 + " > /dev/null");
+        expectCommandToSucceed("dd status=none if=" + fontPath2 + " of=/dev/null");
         // The old file should be still readable.
-        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
+        expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null");
     }
 
     @Test
     public void updateFont_allowSameVersion() throws Exception {
         // Update original font to the same version
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 ORIGINAL_NOTO_COLOR_EMOJI_TTF, ORIGINAL_NOTO_COLOR_EMOJI_TTF_FSV_SIG));
         String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF);
         // Update updated font to the same version
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String fontPath3 = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(fontPath).startsWith(DATA_FONTS_DIR);
@@ -147,21 +164,21 @@
 
     @Test
     public void updateFont_invalidCert() throws Exception {
-        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+        expectCommandToFail(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
     }
 
     @Test
     public void updateFont_downgradeFromSystem() throws Exception {
-        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+        expectCommandToFail(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_V0_TTF, TEST_NOTO_COLOR_EMOJI_V0_TTF_FSV_SIG));
     }
 
     @Test
     public void updateFont_downgradeFromData() throws Exception {
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
-        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+        expectCommandToFail(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
     }
 
@@ -178,7 +195,7 @@
     public void launchApp_afterUpdateFont() throws Exception {
         String originalFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(originalFontPath).startsWith(SYSTEM_FONTS_DIR);
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String updatedFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(updatedFontPath).startsWith(DATA_FONTS_DIR);
@@ -191,57 +208,99 @@
 
     @Test
     public void reboot() throws Exception {
-        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+        expectCommandToSucceed(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
         String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(fontPath).startsWith(DATA_FONTS_DIR);
 
         // Emulate reboot by 'cmd font restart'.
-        expectRemoteCommandToSucceed("cmd font restart");
+        expectCommandToSucceed("cmd font restart");
         String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF);
         assertThat(fontPathAfterReboot).isEqualTo(fontPath);
     }
 
-    private String getFontPath(String fontFileName) throws Exception {
+    private static String insertCert(String certPath) throws Exception {
+        Pair<String, String> result;
+        try (InputStream is = new FileInputStream(certPath)) {
+            result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
+        }
+        // Assert that there are no errors.
+        assertThat(result.second).isEmpty();
+        String keyId = result.first.trim();
+        assertThat(keyId).matches("^\\d+$");
+        return keyId;
+    }
+
+    private static String getFontPath(String fontFileName) throws Exception {
         // TODO: add a dedicated command for testing.
-        String lines = expectRemoteCommandToSucceed("cmd font dump");
+        String lines = expectCommandToSucceed("cmd font dump");
         for (String line : lines.split("\n")) {
             Matcher m = PATTERN_FONT.matcher(line);
             if (m.find() && m.group(1).endsWith(fontFileName)) {
                 return m.group(1);
             }
         }
-        CLog.e("Font not found: " + fontFileName);
-        return null;
+        throw new AssertionError("Font not found: " + fontFileName);
     }
 
-    private void startActivity(String appId, String activityId) throws Exception {
+    private static void startActivity(String appId, String activityId) throws Exception {
         // Make sure that the app is installed and enabled.
         waitUntil(ACTIVITY_TIMEOUT_MILLIS, () -> {
-            String packageInfo = expectRemoteCommandToSucceed(
-                    "pm list packages -e " + EMOJI_RENDERING_TEST_APP_ID);
+            String packageInfo = expectCommandToSucceed("pm list packages -e " + appId);
             return !packageInfo.isEmpty();
         });
-        expectRemoteCommandToSucceed("am force-stop " + EMOJI_RENDERING_TEST_APP_ID);
-        expectRemoteCommandToSucceed("am start-activity -n " + EMOJI_RENDERING_TEST_ACTIVITY);
+        expectCommandToSucceed("am force-stop " + appId);
+        expectCommandToSucceed("am start-activity -n " + activityId);
     }
 
-    private String expectRemoteCommandToSucceed(String cmd) throws Exception {
-        CommandResult result = getDevice().executeShellV2Command(cmd);
-        assertWithMessage("`" + cmd + "` failed: " + result.getStderr())
-                .that(result.getStatus())
-                .isEqualTo(CommandStatus.SUCCESS);
-        return result.getStdout();
+    private static String expectCommandToSucceed(String cmd) throws IOException {
+        Pair<String, String> result = runShellCommand(cmd, null);
+        // UiAutomation.runShellCommand() does not return exit code.
+        // Assume that the command fails if stderr is not empty.
+        assertThat(result.second.trim()).isEmpty();
+        return result.first;
     }
 
-    private void expectRemoteCommandToFail(String cmd) throws Exception {
-        CommandResult result = getDevice().executeShellV2Command(cmd);
-        assertWithMessage("Unexpected success from `" + cmd + "`: " + result.getStderr())
-                .that(result.getStatus())
-                .isNotEqualTo(CommandStatus.SUCCESS);
+    private static void expectCommandToFail(String cmd) throws IOException {
+        Pair<String, String> result = runShellCommand(cmd, null);
+        // UiAutomation.runShellCommand() does not return exit code.
+        // Assume that the command fails if stderr is not empty.
+        assertThat(result.second.trim()).isNotEmpty();
     }
 
-    private void waitUntil(long timeoutMillis, ThrowingSupplier<Boolean> func) {
+    /** Runs a command and returns (stdout, stderr). */
+    private static Pair<String, String> runShellCommand(String cmd, @Nullable InputStream input)
+            throws IOException  {
+        Log.i(TAG, "runShellCommand: " + cmd);
+        UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        ParcelFileDescriptor[] rwe = automation.executeShellCommandRwe(cmd);
+        // executeShellCommandRwe returns [stdout, stdin, stderr].
+        try (ParcelFileDescriptor outFd = rwe[0];
+             ParcelFileDescriptor inFd = rwe[1];
+             ParcelFileDescriptor errFd = rwe[2]) {
+            if (input != null) {
+                try (OutputStream os = new FileOutputStream(inFd.getFileDescriptor())) {
+                    StreamUtil.copyStreams(input, os);
+                }
+            }
+            // We have to close stdin before reading stdout and stderr.
+            // It's safe to close ParcelFileDescriptor multiple times.
+            inFd.close();
+            String stdout;
+            try (InputStream is = new FileInputStream(outFd.getFileDescriptor())) {
+                stdout = StreamUtil.readInputStream(is);
+            }
+            Log.i(TAG, "stdout =  " + stdout);
+            String stderr;
+            try (InputStream is = new FileInputStream(errFd.getFileDescriptor())) {
+                stderr = StreamUtil.readInputStream(is);
+            }
+            Log.i(TAG, "stderr =  " + stderr);
+            return new Pair<>(stdout, stderr);
+        }
+    }
+
+    private static void waitUntil(long timeoutMillis, ThrowingSupplier<Boolean> func) {
         long untilMillis = System.currentTimeMillis() + timeoutMillis;
         do {
             try {
@@ -256,25 +315,16 @@
         throw new AssertionError("Timed out");
     }
 
-    private boolean isFileOpenedBy(String path, String appId) throws DeviceNotAvailableException {
+    private static boolean isFileOpenedBy(String path, String appId) throws Exception {
         String pid = pidOf(appId);
         if (pid.isEmpty()) {
             return false;
         }
-        CommandResult result = getDevice().executeShellV2Command(
-                String.format("lsof -t -p %s '%s'", pid, path));
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            return false;
-        }
-        // The file is open if the output of lsof is non-empty.
-        return !result.getStdout().trim().isEmpty();
+        String cmd = String.format("lsof -t -p %s %s", pid, path);
+        return !expectCommandToSucceed(cmd).trim().isEmpty();
     }
 
-    private String pidOf(String appId) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command("pidof " + appId);
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            return "";
-        }
-        return result.getStdout().trim();
+    private static String pidOf(String appId) throws Exception {
+        return expectCommandToSucceed("pidof " + appId).trim();
     }
 }