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

Commit 2c973f16 authored by Kohsuke Yatoh's avatar Kohsuke Yatoh Committed by Android (Google) Code Review
Browse files

Merge "Verify updated font is used in app process." into sc-dev

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