blob: 76fc1f945180a20e45a8a9e746fadfbade2f4889 [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.device.collectors;
import static org.junit.Assert.assertNotNull;
import android.device.collectors.annotations.OptionClass;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.uiautomator.UiDevice;
import java.io.IOException;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.junit.runner.Description;
/**
* A {@link BaseMetricListener} that captures video of the screen.
*
* <p>This class needs external storage permission. See {@link BaseMetricListener} how to grant
* external storage permission, especially at install time.
*/
@OptionClass(alias = "screen-record-collector")
public class ScreenRecordCollector extends BaseMetricListener {
// Quality is relative to screen resolution.
// * "medium" is 1/2 the resolution.
// * "low" is 1/8 the resolution.
// * Otherwise, use the resolution.
@VisibleForTesting static final String QUALITY_ARG = "video-quality";
// Maximum parts per test (each part is <= 3min).
@VisibleForTesting static final int MAX_RECORDING_PARTS = 5;
private static final long VIDEO_TAIL_BUFFER = 500;
static final String OUTPUT_DIR = "run_listeners/videos";
private UiDevice mDevice;
private static File mDestDir;
private RecordingThread mCurrentThread;
private String mVideoDimensions;
// Tracks the test iterations to ensure that each failure gets unique filenames.
// Key: test description; value: number of iterations.
private Map<String, Integer> mTestIterations = new HashMap<String, Integer>();
public ScreenRecordCollector() {
super();
}
/** Constructors for overriding instrumentation arguments only. */
@VisibleForTesting
ScreenRecordCollector(Bundle args) {
super(args);
}
@Override
public void onTestRunStart(DataRecord runData, Description description) {
mDestDir = createAndEmptyDirectory(OUTPUT_DIR);
try {
long scaleDown = 1;
switch (getArgsBundle().getString(QUALITY_ARG, "default")) {
case "high":
scaleDown = 1;
break;
case "medium":
scaleDown = 2;
break;
case "low":
scaleDown = 8;
break;
default:
return;
}
// Display metrics isn't the absolute size, so use "wm size".
String[] dims =
getDevice()
.executeShellCommand("wm size")
.substring("Physical size: ".length())
.trim()
.split("x");
int width = Integer.parseInt(dims[0]);
int height = Integer.parseInt(dims[1]);
mVideoDimensions = String.format("%dx%d", width / scaleDown, height / scaleDown);
Log.v(getTag(), String.format("Using video dimensions: %s", mVideoDimensions));
} catch (Exception e) {
Log.e(getTag(), "Failed to query the device dimensions. Using default.", e);
}
}
@Override
public void onTestStart(DataRecord testData, Description description) {
if (mDestDir == null) {
return;
}
// Track the number of iteration for this test.
amendIterations(description);
// Start the screen recording operation.
mCurrentThread = new RecordingThread("test-screen-record", description);
mCurrentThread.start();
}
@Override
public void onTestEnd(DataRecord testData, Description description) {
// Skip if not directory.
if (mDestDir == null) {
return;
}
// Add some extra time to the video end.
SystemClock.sleep(getTailBuffer());
// Ctrl + C all screen record processes.
mCurrentThread.cancel();
// Wait for the thread to completely die.
try {
mCurrentThread.join();
} catch (InterruptedException ex) {
Log.e(getTag(), "Interrupted when joining the recording thread.", ex);
}
// Add the output files to the data record.
for (File recording : mCurrentThread.getRecordings()) {
Log.d(getTag(), String.format("Adding video part: #%s", recording.getName()));
testData.addFileMetric(
String.format("%s_%s", getTag(), recording.getName()), recording);
}
// TODO(b/144869954): Delete when tests pass.
}
/** Updates the number of iterations performed for a given test {@link Description}. */
private void amendIterations(Description description) {
String testName = description.getDisplayName();
mTestIterations.computeIfPresent(testName, (name, iterations) -> iterations + 1);
mTestIterations.computeIfAbsent(testName, name -> 1);
}
/** Returns the recording's name for part {@code part} of test {@code description}. */
private File getOutputFile(Description description, int part) {
final String baseName =
String.format("%s.%s", description.getClassName(), description.getMethodName());
// Omit the iteration number for the first iteration.
int iteration = mTestIterations.get(description.getDisplayName());
final String fileName =
String.format(
"%s-video%s.mp4",
iteration == 1
? baseName
: String.join("-", baseName, String.valueOf(iteration)),
part == 1 ? "" : part);
return Paths.get(mDestDir.getAbsolutePath(), fileName).toFile();
}
/** Returns a buffer duration for the end of the video. */
@VisibleForTesting
public long getTailBuffer() {
return VIDEO_TAIL_BUFFER;
}
/** Returns the currently active {@link UiDevice}. */
public UiDevice getDevice() {
if (mDevice == null) {
mDevice = UiDevice.getInstance(getInstrumentation());
}
return mDevice;
}
private class RecordingThread extends Thread {
private final Description mDescription;
private final List<File> mRecordings;
private boolean mContinue;
public RecordingThread(String name, Description description) {
super(name);
mContinue = true;
mRecordings = new ArrayList<>();
assertNotNull("No test description provided for recording.", description);
mDescription = description;
}
@Override
public void run() {
try {
// Start at i = 1 to encode parts as X.mp4, X2.mp4, X3.mp4, etc.
for (int i = 1; i <= MAX_RECORDING_PARTS && mContinue; i++) {
File output = getOutputFile(mDescription, i);
Log.d(
getTag(),
String.format("Recording screen to %s", output.getAbsolutePath()));
mRecordings.add(output);
// Make sure not to block on this background command in the main thread so
// that the test continues to run, but block in this thread so it does not
// trigger a new screen recording session before the prior one completes.
String dimensionsOpt =
mVideoDimensions == null
? ""
: String.format("--size=%s", mVideoDimensions);
getDevice()
.executeShellCommand(
String.format(
"screenrecord %s %s",
dimensionsOpt, output.getAbsolutePath()));
}
} catch (IOException e) {
throw new RuntimeException("Caught exception while screen recording.");
}
}
public void cancel() {
mContinue = false;
// Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
try {
String[] pids = getDevice().executeShellCommand("pidof screenrecord").split(" ");
for (String pid : pids) {
// Avoid empty process ids, because of weird splitting behavior.
if (pid.isEmpty()) {
continue;
}
getDevice().executeShellCommand(String.format("kill -2 %s", pid));
Log.d(
getTag(),
String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
}
} catch (IOException e) {
throw new RuntimeException("Failed to kill screen recording process.");
}
}
public List<File> getRecordings() {
return mRecordings;
}
private String getTag() {
return RecordingThread.class.getName();
}
}
}