Skip to content

Commit 81c5c62

Browse files
OmniLab Teamcopybara-github
authored andcommitted
Internal change
PiperOrigin-RevId: 918499740
1 parent 5f73fcd commit 81c5c62

7 files changed

Lines changed: 366 additions & 19 deletions

File tree

src/java/com/google/devtools/mobileharness/api/model/error/AndroidErrorId.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,12 +723,16 @@ public enum AndroidErrorId implements ErrorId {
723723
ANDROID_SHIPPING_API_LEVEL_CHECK_DECORATOR_INVALID_VENDOR_MIN_API_LEVEL(
724724
130_003, ErrorType.CUSTOMER_ISSUE),
725725

726-
// Android Logcat Monitoring Decorator: 130_051 ~ 130_100
726+
// Android Logcat Monitoring Decorator: 130_051 ~ 130_075
727727
ANDROID_LOGCAT_MONITORING_DECORATOR_INFRA_PROCESS_CRASHED(130_051, ErrorType.INFRA_ISSUE),
728728
ANDROID_LOGCAT_MONITORING_DECORATOR_ORCHESTRATOR_CONNECTION_FAILURE(
729729
130_052, ErrorType.INFRA_ISSUE),
730730
ANDROID_LOGCAT_MONITORING_DECORATOR_CRASH_DIALOG_DETECTED(130_053, ErrorType.INFRA_ISSUE),
731731

732+
// Android Emulator Video Decorator: 130_076 ~ 130_100
733+
ANDROID_EMULATOR_VIDEO_DECORATOR_VIDEO_FILE_ABSENT(130_075, ErrorType.INFRA_ISSUE),
734+
ANDROID_EMULATOR_VIDEO_DECORATOR_VIDEO_FILE_EMPTY(130_076, ErrorType.INFRA_ISSUE),
735+
732736
// AndroidFlashGkiDecorator: 130_101 ~ 130_150
733737
ANDROID_FLASH_GKI_DECORATOR_GENERATE_GKI_BOOT_IMG_ERROR(130_101, ErrorType.DEPENDENCY_ISSUE),
734738
ANDROID_FLASH_GKI_DECORATOR_FILE_NOT_FOUND(130_102, ErrorType.CUSTOMER_ISSUE),

src/java/com/google/wireless/qa/mobileharness/shared/api/decorator/AndroidEmulatorVideoDecorator.java

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,151 @@
1616

1717
package com.google.wireless.qa.mobileharness.shared.api.decorator;
1818

19+
import static com.google.common.collect.ImmutableList.toImmutableList;
20+
21+
import com.google.common.base.Joiner;
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.collect.Sets;
1924
import com.google.common.flogger.FluentLogger;
25+
import com.google.devtools.deviceinfra.platform.android.lightning.internal.sdk.adb.Adb;
26+
import com.google.devtools.mobileharness.api.model.error.AndroidErrorId;
2027
import com.google.devtools.mobileharness.api.model.error.MobileHarnessException;
28+
import com.google.devtools.mobileharness.shared.util.time.Sleeper;
2129
import com.google.wireless.qa.mobileharness.shared.api.driver.Driver;
2230
import com.google.wireless.qa.mobileharness.shared.model.job.TestInfo;
2331
import com.google.wireless.qa.mobileharness.shared.model.job.in.spec.SpecConfigable;
2432
import com.google.wireless.qa.mobileharness.shared.proto.spec.decorator.AndroidEmulatorVideoDecoratorSpec;
33+
import java.nio.file.Path;
34+
import java.time.Duration;
35+
import java.util.Set;
36+
import java.util.concurrent.atomic.AtomicBoolean;
37+
import java.util.concurrent.atomic.AtomicInteger;
38+
import javax.inject.Inject;
2539

2640
/** Decorator for recording video on Android Emulators using the console recording */
27-
public class AndroidEmulatorVideoDecorator extends BaseDecorator
41+
public class AndroidEmulatorVideoDecorator extends AsyncTimerDecorator
2842
implements SpecConfigable<AndroidEmulatorVideoDecoratorSpec> {
2943

3044
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
45+
private static final Duration MAX_EMULATOR_SUPPORTED_VIDEO_DURATION = Duration.ofMinutes(15);
46+
private static final int DEFAULT_FPS = 5;
47+
private static final int DEFAULT_BIT_RATE = 100000;
48+
49+
private final Adb adb;
50+
private final Sleeper sleeper;
51+
private final AtomicInteger videoCount = new AtomicInteger(0);
52+
53+
private final AtomicBoolean running = new AtomicBoolean(false);
54+
55+
private final Set<Path> generatedFiles = Sets.newConcurrentHashSet();
3156

32-
public AndroidEmulatorVideoDecorator(Driver decorated, TestInfo testInfo) {
57+
private AndroidEmulatorVideoDecoratorSpec emulatorVideoDecoratorSpec;
58+
59+
@Inject
60+
AndroidEmulatorVideoDecorator(Driver decorated, TestInfo testInfo, Adb adb, Sleeper sleeper) {
3361
super(decorated, testInfo);
62+
this.adb = adb;
63+
this.sleeper = sleeper;
3464
}
3565

3666
@Override
37-
public void run(TestInfo testInfo) throws MobileHarnessException, InterruptedException {
38-
AndroidEmulatorVideoDecoratorSpec spec = testInfo.jobInfo().combinedSpec(this);
67+
public void onStart(TestInfo testInfo) throws MobileHarnessException, InterruptedException {
68+
emulatorVideoDecoratorSpec = testInfo.jobInfo().combinedSpec(this);
69+
}
3970

71+
@Override
72+
long getIntervalMs(TestInfo testInfo) {
73+
return emulatorVideoDecoratorSpec.hasTimeLimitSecs()
74+
? Duration.ofSeconds(emulatorVideoDecoratorSpec.getTimeLimitSecs()).toMillis()
75+
: MAX_EMULATOR_SUPPORTED_VIDEO_DURATION.toMillis();
76+
}
77+
78+
@Override
79+
void runTimerTask(TestInfo testInfo) throws MobileHarnessException, InterruptedException {
80+
// If a recording is already running, stop it before starting a new one. The video file is only
81+
// written to when the recording is explicitly stopped. Allow a small delay before starting the
82+
// next recording to let the emulator complete writing the previous video file. If consecutive
83+
// video recordings are started too quickly, the emulator may not have finished writing the
84+
// previous video file, resulting in a corrupt or empty video file.
85+
if (running.get()) {
86+
stopVideoRecording(testInfo);
87+
}
88+
var device = getDevice();
89+
var fps =
90+
emulatorVideoDecoratorSpec.hasFps() ? emulatorVideoDecoratorSpec.getFps() : DEFAULT_FPS;
91+
var bitRate =
92+
emulatorVideoDecoratorSpec.hasBitRate()
93+
? emulatorVideoDecoratorSpec.getBitRate()
94+
: DEFAULT_BIT_RATE;
95+
var timeLimitSecs =
96+
emulatorVideoDecoratorSpec.hasTimeLimitSecs()
97+
? emulatorVideoDecoratorSpec.getTimeLimitSecs()
98+
: MAX_EMULATOR_SUPPORTED_VIDEO_DURATION.toSeconds();
99+
var videoOutputPath =
100+
Path.of(testInfo.getGenFileDir())
101+
.resolve(String.format("video-%d.webm", videoCount.incrementAndGet()));
102+
var args =
103+
ImmutableList.of(
104+
"emu",
105+
"screenrecord",
106+
"start",
107+
"--bit-rate",
108+
String.valueOf(bitRate),
109+
"--fps",
110+
String.valueOf(fps),
111+
"--time-limit",
112+
String.valueOf(timeLimitSecs),
113+
videoOutputPath.toString());
114+
115+
var output = adb.run(device.getDeviceId(), args.toArray(String[]::new));
116+
running.set(true);
117+
generatedFiles.add(videoOutputPath);
40118
testInfo
41119
.log()
42120
.atInfo()
43121
.alsoTo(logger)
44-
.log("------- Starting Android Emulator Video Decorator --------\n Spec: %s", spec);
122+
.log("------- Emulator screenrecord start --------\n Output: %s", output);
123+
}
45124

46-
getDecorated().run(testInfo);
125+
private void stopVideoRecording(TestInfo testInfo)
126+
throws MobileHarnessException, InterruptedException {
127+
var videoOutputPath =
128+
Path.of(testInfo.getGenFileDir()).resolve(String.format("video-%d.webm", videoCount.get()));
129+
var args = ImmutableList.of("emu", "screenrecord", "stop", videoOutputPath.toString());
130+
var unused = adb.run(getDevice().getDeviceId(), args.toArray(String[]::new));
131+
// Sleep for 1 second to let the emulator complete writing the video file.
132+
sleeper.sleep(Duration.ofSeconds(1));
133+
running.set(false);
134+
}
47135

136+
@Override
137+
void onEnd(TestInfo testInfo) throws MobileHarnessException, InterruptedException {
138+
stopVideoRecording(testInfo);
139+
// Sleep for 2 seconds to let the emulator complete writing the video file.
140+
sleeper.sleep(Duration.ofSeconds(2));
48141
testInfo
49142
.log()
50143
.atInfo()
51144
.alsoTo(logger)
52-
.log("------- Stopping Android Emulator Video Decorator --------");
145+
.log("------- Stopped Android Emulator Video Decorator --------");
146+
147+
var missingFiles =
148+
generatedFiles.stream()
149+
.filter(videoFile -> !videoFile.toFile().exists())
150+
.collect(toImmutableList());
151+
if (!missingFiles.isEmpty()) {
152+
throw new MobileHarnessException(
153+
AndroidErrorId.ANDROID_EMULATOR_VIDEO_DECORATOR_VIDEO_FILE_ABSENT,
154+
"Generated video files absent. Expected: " + Joiner.on(",").join(missingFiles));
155+
}
156+
var emptyFiles =
157+
generatedFiles.stream()
158+
.filter(videoFile -> videoFile.toFile().length() == 0L)
159+
.collect(toImmutableList());
160+
if (!emptyFiles.isEmpty()) {
161+
throw new MobileHarnessException(
162+
AndroidErrorId.ANDROID_EMULATOR_VIDEO_DECORATOR_VIDEO_FILE_EMPTY,
163+
"Generated video files empty. Empty files: " + Joiner.on(",").join(emptyFiles));
164+
}
53165
}
54166
}

src/java/com/google/wireless/qa/mobileharness/shared/api/decorator/BUILD

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,17 @@ java_library(
260260
name = "android_emulator_video_decorator",
261261
srcs = ["AndroidEmulatorVideoDecorator.java"],
262262
deps = [
263-
":base_decorator",
263+
":async_timer_decorator",
264+
"//src/java/com/google/devtools/deviceinfra/platform/android/lightning/internal/sdk/adb",
264265
"//src/java/com/google/devtools/mobileharness/api/model/error",
265266
"//src/java/com/google/devtools/mobileharness/shared/util/logging:google_logger",
267+
"//src/java/com/google/devtools/mobileharness/shared/util/time:sleeper",
266268
"//src/java/com/google/wireless/qa/mobileharness/shared/api/driver",
267269
"//src/java/com/google/wireless/qa/mobileharness/shared/model/job",
268270
"//src/java/com/google/wireless/qa/mobileharness/shared/model/job/in/spec",
269271
"//src/java/com/google/wireless/qa/mobileharness/shared/proto/spec:android_emulator_video_decorator_spec_java_proto",
272+
"@maven//:com_google_guava_guava",
273+
"@maven//:javax_inject_jsr330_api",
270274
],
271275
)
272276

src/java/com/google/wireless/qa/mobileharness/shared/api/validator/job/AndroidEmulatorVideoDecoratorJobValidator.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,19 @@ public ImmutableList<String> validate(JobInfo job) throws InterruptedException {
4646
return errorsBuilder.build();
4747
}
4848
for (AndroidEmulatorVideoDecoratorSpec spec : specs) {
49-
if (spec.getFps() < 0) {
49+
if (spec.hasFps() && spec.getFps() < 0) {
5050
errorsBuilder.add("Invalid fps value. Negative value supplied: " + spec.getFps());
5151
}
52-
if (spec.getBitRate() < 0) {
53-
errorsBuilder.add("Invalid bit_rate value. Negative value supplied: " + spec.getBitRate());
52+
if (spec.hasBitRate() && (spec.getBitRate() < 100000 || spec.getBitRate() > 2500000)) {
53+
errorsBuilder.add(
54+
"Invalid bit_rate value. Should be between 100000 and 2500000. Supplied: "
55+
+ spec.getBitRate());
5456
}
55-
if (spec.getTimeLimitSecs() < 0) {
57+
if (spec.hasTimeLimitSecs()
58+
&& (spec.getTimeLimitSecs() < 0 || spec.getTimeLimitSecs() > 900 /* 15 minutes */)) {
5659
errorsBuilder.add(
57-
"Invalid time_limit_secs value. Negative value supplied: " + spec.getTimeLimitSecs());
60+
"Invalid time_limit_secs value. Should be between 0 and 900 (15 minutes). Supplied: "
61+
+ spec.getTimeLimitSecs());
5862
}
5963
}
6064
return errorsBuilder.build();

0 commit comments

Comments
 (0)