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();
+ }
}