Verify updated font is used in app process.

This CL adds a VTS test that:
1. Updates NotoColorEmoji font
2. Launches a test app that renders an emoji
3. Verifies that the updated NotoColorEmoji font file is used by the app
   process.

Bug: 180370569
Test: atest UpdatableSystemFontTest
Change-Id: I418d7cc23a290ebe4ae6e5b8af782b336497fbdd
diff --git a/tests/UpdatableSystemFontTest/AndroidTest.xml b/tests/UpdatableSystemFontTest/AndroidTest.xml
index d573e93..4f11669 100644
--- a/tests/UpdatableSystemFontTest/AndroidTest.xml
+++ b/tests/UpdatableSystemFontTest/AndroidTest.xml
@@ -19,6 +19,11 @@
     <!-- This test requires root to side load fs-verity cert. -->
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
 
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="EmojiRenderingTestApp.apk" />
+    </target_preparer>
+
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
         <option name="push" value="UpdatableSystemFontTestCert.der->/data/local/tmp/UpdatableSystemFontTestCert.der" />
diff --git a/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp
new file mode 100644
index 0000000..ed34fa9
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp
@@ -0,0 +1,32 @@
+// 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.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+    name: "EmojiRenderingTestApp",
+    manifest: "AndroidManifest.xml",
+    srcs: ["src/**/*.java"],
+    test_suites: [
+        "general-tests",
+        "vts",
+    ],
+}
diff --git a/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/AndroidManifest.xml b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..5d8f5fc
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?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.emojirenderingtestapp">
+    <application>
+        <activity android:name=".EmojiRenderingTestActivity"/>
+    </application>
+</manifest>
diff --git a/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/src/com/android/emojirenderingtestapp/EmojiRenderingTestActivity.java b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/src/com/android/emojirenderingtestapp/EmojiRenderingTestActivity.java
new file mode 100644
index 0000000..947e9c2
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/src/com/android/emojirenderingtestapp/EmojiRenderingTestActivity.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package com.android.emojirenderingtestapp;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/** Test app to render an emoji. */
+public class EmojiRenderingTestActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        LinearLayout container = new LinearLayout(this);
+        container.setOrientation(LinearLayout.VERTICAL);
+        TextView textView = new TextView(this);
+        textView.setText("\uD83E\uDD72"); // 🥲
+        container.addView(textView, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+        setContentView(container);
+    }
+}
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index e684556..032da3f 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -36,7 +36,6 @@
 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;
 
@@ -47,6 +46,9 @@
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
 
+    private static final String SYSTEM_FONTS_DIR = "/system/fonts/";
+    private static final String DATA_FONTS_DIR = "/data/fonts/files/";
+
     private static final String CERT_PATH = "/data/local/tmp/UpdatableSystemFontTestCert.der";
 
     private static final Pattern PATTERN_FONT = Pattern.compile("path = ([^, \n]*)");
@@ -72,6 +74,14 @@
     private static final String TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG =
             "/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig";
 
+    private static final String EMOJI_RENDERING_TEST_APP_ID = "com.android.emojirenderingtestapp";
+    private static final String EMOJI_RENDERING_TEST_ACTIVITY =
+            EMOJI_RENDERING_TEST_APP_ID + "/.EmojiRenderingTestActivity";
+
+    private interface ThrowingSupplier<T> {
+        T get() throws Exception;
+    }
+
     @Rule
     public final AddFsVerityCertRule mAddFsverityCertRule =
             new AddFsVerityCertRule(this, CERT_PATH);
@@ -91,7 +101,10 @@
         expectRemoteCommandToSucceed(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/files/");
+        assertThat(fontPath).startsWith(DATA_FONTS_DIR);
+        // The updated font should be readable and unmodifiable.
+        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
+        expectRemoteCommandToFail("echo -n '' >> " + fontPath);
     }
 
     @Test
@@ -102,8 +115,12 @@
         expectRemoteCommandToSucceed(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/files/");
+        assertThat(fontPath2).startsWith(DATA_FONTS_DIR);
         assertThat(fontPath2).isNotEqualTo(fontPath);
+        // The new file should be readable.
+        expectRemoteCommandToSucceed("cat " + fontPath2 + " > /dev/null");
+        // The old file should be still readable.
+        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
     }
 
     @Test
@@ -119,25 +136,14 @@
         expectRemoteCommandToSucceed(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/files/");
+        assertThat(fontPath).startsWith(DATA_FONTS_DIR);
         assertThat(fontPath2).isNotEqualTo(fontPath);
-        assertThat(fontPath2).startsWith("/data/fonts/files/");
-        assertThat(fontPath3).startsWith("/data/fonts/files/");
+        assertThat(fontPath2).startsWith(DATA_FONTS_DIR);
+        assertThat(fontPath3).startsWith(DATA_FONTS_DIR);
         assertThat(fontPath3).isNotEqualTo(fontPath);
     }
 
     @Test
-    public void updatedFont_dataFileIsImmutableAndReadable() throws Exception {
-        expectRemoteCommandToSucceed(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");
-
-        expectRemoteCommandToFail("echo -n '' >> " + fontPath);
-        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
-    }
-
-    @Test
     public void updateFont_invalidCert() throws Exception {
         expectRemoteCommandToFail(String.format("cmd font update %s %s",
                 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
@@ -158,11 +164,37 @@
     }
 
     @Test
+    public void launchApp() throws Exception {
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath).startsWith(SYSTEM_FONTS_DIR);
+        expectRemoteCommandToSucceed("am force-stop " + EMOJI_RENDERING_TEST_APP_ID);
+        expectRemoteCommandToSucceed("am start-activity -n " + EMOJI_RENDERING_TEST_ACTIVITY);
+        waitUntil(TimeUnit.SECONDS.toMillis(5), () ->
+                isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID));
+    }
+
+    @Test
+    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",
+                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);
+        expectRemoteCommandToSucceed("am force-stop " + EMOJI_RENDERING_TEST_APP_ID);
+        expectRemoteCommandToSucceed("am start-activity -n " + EMOJI_RENDERING_TEST_ACTIVITY);
+        // The original font should NOT be opened by the app.
+        waitUntil(TimeUnit.SECONDS.toMillis(5), () ->
+                isFileOpenedBy(updatedFontPath, EMOJI_RENDERING_TEST_APP_ID)
+                        && !isFileOpenedBy(originalFontPath, EMOJI_RENDERING_TEST_APP_ID));
+    }
+
+    @Test
     public void reboot() throws Exception {
         expectRemoteCommandToSucceed(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/files/");
+        assertThat(fontPath).startsWith(DATA_FONTS_DIR);
 
         expectRemoteCommandToSucceed("stop");
         expectRemoteCommandToSucceed("start");
@@ -210,16 +242,40 @@
         });
     }
 
-    private void waitUntil(long timeoutMillis, Supplier<Boolean> func) {
+    private void waitUntil(long timeoutMillis, ThrowingSupplier<Boolean> func) {
         long untilMillis = System.currentTimeMillis() + timeoutMillis;
         do {
-            if (func.get()) return;
             try {
+                if (func.get()) return;
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 throw new AssertionError("Interrupted", e);
+            } catch (Exception e) {
+                throw new AssertionError("Unexpected exception", e);
             }
         } while (System.currentTimeMillis() < untilMillis);
         throw new AssertionError("Timed out");
     }
+
+    private boolean isFileOpenedBy(String path, String appId) throws DeviceNotAvailableException {
+        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();
+    }
+
+    private String pidOf(String appId) throws DeviceNotAvailableException {
+        CommandResult result = getDevice().executeShellV2Command("pidof " + appId);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            return "";
+        }
+        return result.getStdout().trim();
+    }
 }