chromium/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/ArTestRuleUtils.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.vr.util;

import org.junit.Assert;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.vr.rules.ArPlaybackFile;
import org.chromium.chrome.browser.vr.rules.ArTestRule;
import org.chromium.chrome.browser.vr.rules.ChromeTabbedActivityArTestRule;
import org.chromium.chrome.browser.vr.rules.CustomTabActivityArTestRule;
import org.chromium.chrome.browser.vr.rules.WebappActivityArTestRule;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;

/** Utility class for interacting with AR-specific rules. */
public class ArTestRuleUtils extends XrTestRuleUtils {
    private static final String DEFAULT_PLAYBACK_FILEPATH =
            "chrome/test/data/xr/ar_playback_datasets/floor_session_12s_30fps.mp4";
    // This must be kept in sync with the ArCore session_settings entry in
    // //chrome/android/javatests/AndroidManifest_monochrome.xml.
    private static final String PLAYBACK_FILE_LOCATION =
            UrlUtils.getIsolatedTestFilePath("chrome/test/data/xr/dataset.mp4");

    private static final int BLOCK_SIZE = 2048;

    /**
     * Helper method to apply an ArTestRule/ChromeActivityTestRule combination. The only difference
     * between various classes that implement ArTestRule is how they start their activity, so the
     * common boilerplate code can be kept here so each ArTestRule only has to provide a way to
     * launch Chrome.
     *
     * @param base The Statement passed to the calling ChromeActivityRule's apply() method.
     * @param desc The Description passed to the calling ChromeActivityTestRule's apply() method.
     * @param rule The calling ArTestRule.
     * @param launcher A ChromeLaunchMethod whose launch() contains the code snippet to start Chrome
     *     in the calling ChromeActivityTestRule's activity type.
     */
    public static void evaluateArTestRuleImpl(
            final Statement base,
            final Description desc,
            final ArTestRule rule,
            final ChromeLaunchMethod launcher)
            throws Throwable {
        // See if the test is annotated with @ArPlaybackFile
        String playbackFilepath = DEFAULT_PLAYBACK_FILEPATH;
        ArPlaybackFile annotation = desc.getAnnotation(ArPlaybackFile.class);
        if (annotation != null) playbackFilepath = annotation.value();
        playbackFilepath = UrlUtils.getIsolatedTestFilePath(playbackFilepath);

        // Check that the playback file actually exists, and if so, copy it to the location that's
        // hard-coded into the manifest so that ArCore can find and use it.
        File playbackFile = new File(playbackFilepath);
        Assert.assertTrue(
                "Given AR playback file " + playbackFilepath + " does not exist.",
                playbackFile.exists());
        Assert.assertFalse(
                "Given AR playback file " + playbackFilepath + " is a directory.",
                playbackFile.isDirectory());
        copyDatasetIfNecessary(playbackFile, new File(PLAYBACK_FILE_LOCATION));

        launcher.launch();
        base.evaluate();
    }

    /**
     * Creates the list of ArTestRules that are currently supported for use in test
     * parameterization.
     *
     * @return An ArrayList of ParameterSets, with each ParameterSet containing a callable to create
     *     an ArTestRule for a supported ChromeActivity.
     */
    public static ArrayList<ParameterSet> generateDefaultTestRuleParameters() {
        ArrayList<ParameterSet> parameters = new ArrayList<ParameterSet>();
        parameters.add(
                new ParameterSet()
                        .value(
                                new Callable<ChromeTabbedActivityArTestRule>() {
                                    @Override
                                    public ChromeTabbedActivityArTestRule call() {
                                        return new ChromeTabbedActivityArTestRule();
                                    }
                                })
                        .name("ChromeTabbedActivity"));
        parameters.add(
                new ParameterSet()
                        .value(
                                new Callable<CustomTabActivityArTestRule>() {
                                    @Override
                                    public CustomTabActivityArTestRule call() {
                                        return new CustomTabActivityArTestRule();
                                    }
                                })
                        .name("CustomTabActivity"));
        parameters.add(
                new ParameterSet()
                        .value(
                                new Callable<WebappActivityArTestRule>() {
                                    @Override
                                    public WebappActivityArTestRule call() {
                                        return new WebappActivityArTestRule();
                                    }
                                })
                        .name("WebappActivity"));
        return parameters;
    }

    /**
     * Copies |src| to |dst| if the contents of the two are not already identical.
     *
     * @param src The File to copy data from.
     * @param dst The File to copy data to.
     */
    private static void copyDatasetIfNecessary(File src, File dst)
            throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        if (!dst.exists()) {
            copyDataset(src, dst);
            return;
        }
        if (!Arrays.equals(getFileDigest(src), getFileDigest(dst))) {
            copyDataset(src, dst);
        }
    }

    /**
     * Computes the SHA-1 digest of |src|.
     *
     * @param src The File to compute the digest of.
     * @return A byte array containing the SHA-1 digest of |src|.
     */
    private static byte[] getFileDigest(File src)
            throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        try (FileInputStream in = new FileInputStream(src)) {
            byte[] buf = new byte[BLOCK_SIZE];
            int len;
            while ((len = in.read(buf)) > 0) {
                md.update(buf, 0, len);
            }
        }
        return md.digest();
    }

    /**
     * Copies |src| to |dst|.
     *
     * @param src The file to copy data from.
     * @param dst The file to copy data to.
     */
    private static void copyDataset(File src, File dst) throws FileNotFoundException, IOException {
        // Files.copy() would be much simpler than this, but is only available on API version 26+.
        try (FileInputStream in = new FileInputStream(src)) {
            try (FileOutputStream out = new FileOutputStream(dst)) {
                byte[] buf = new byte[BLOCK_SIZE];
                int len;
                while ((len = in.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }
            }
        }
    }
}