Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0c113ea authored by Kohsuke Yatoh's avatar Kohsuke Yatoh
Browse files

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
parent 55c108fd
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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" />
+32 −0
Original line number Diff line number Diff line
// 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",
    ],
}
+23 −0
Original line number Diff line number Diff line
<?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>
+40 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+76 −20
Original line number Diff line number Diff line
@@ -36,7 +36,6 @@ 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;

@@ -47,6 +46,9 @@ import java.util.regex.Pattern;
@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 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
    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 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
        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 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
        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,24 +136,13 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
        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",
@@ -157,12 +163,38 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
                TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
    }

    @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 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
        });
    }

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