1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.media.tests;
17 
18 import com.android.ddmlib.CollectingOutputReceiver;
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.device.IFileEntry;
22 import com.android.tradefed.device.ITestDevice;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.result.ITestInvocationListener;
25 import com.android.tradefed.result.TestDescription;
26 import com.android.tradefed.testtype.IDeviceTest;
27 import com.android.tradefed.testtype.IRemoteTest;
28 import com.android.tradefed.util.CommandResult;
29 import com.android.tradefed.util.CommandStatus;
30 import com.android.tradefed.util.FileUtil;
31 import com.android.tradefed.util.RunUtil;
32 
33 import java.io.File;
34 import java.text.ParseException;
35 import java.text.SimpleDateFormat;
36 import java.util.Arrays;
37 import java.util.Date;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.TimeZone;
41 import java.util.concurrent.TimeUnit;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 
45 /**
46  * Tests adb command "screenrecord", i.e. "adb screenrecord [--size] [--bit-rate] [--time-limit]"
47  *
48  * <p>The test use the above command to record a video of DUT's screen. It then tries to verify that
49  * a video was actually recorded and that the video is a valid video file. It currently uses
50  * 'avprobe' to do the video analysis along with extracting parameters from the adb command's
51  * output.
52  */
53 public class AdbScreenrecordTest implements IDeviceTest, IRemoteTest {
54 
55     //===================================================================
56     // TEST OPTIONS
57     //===================================================================
58     @Option(name = "run-key", description = "Run key for the test")
59     private String mRunKey = "AdbScreenRecord";
60 
61     @Option(name = "time-limit", description = "Recording time in seconds", isTimeVal = true)
62     private long mRecordTimeInSeconds = -1;
63 
64     @Option(name = "size", description = "Video Size: 'widthxheight', e.g. '1280x720'")
65     private String mVideoSize = null;
66 
67     @Option(name = "bit-rate", description = "Video bit rate in megabits per second, e.g. 4000000")
68     private long mBitRate = -1;
69 
70     //===================================================================
71     // CLASS VARIABLES
72     //===================================================================
73     private ITestDevice mDevice;
74     private TestRunHelper mTestRunHelper;
75 
76     //===================================================================
77     // CONSTANTS
78     //===================================================================
79     private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
80     private static final long DEVICE_SYNC_MS = 5 * 60 * 1000; // 5 min
81     private static final long POLLING_INTERVAL_MS = 5 * 1000; // 5 sec
82     private static final long CMD_TIMEOUT_MS = 5 * 1000; // 5 sec
83     private static final String ERR_OPTION_MALFORMED = "Test option %1$s is not correct [%2$s]";
84     private static final String OPTION_TIME_LIMIT = "--time-limit";
85     private static final String OPTION_SIZE = "--size";
86     private static final String OPTION_BITRATE = "--bit-rate";
87     private static final String RESULT_KEY_RECORDED_FRAMES = "recorded_frames";
88     private static final String RESULT_KEY_RECORDED_LENGTH = "recorded_length";
89     private static final String RESULT_KEY_VERIFIED_DURATION = "verified_duration";
90     private static final String RESULT_KEY_VERIFIED_BITRATE = "verified_bitrate";
91     private static final String TEST_FILE = "/sdcard/screenrecord_test.mp4";
92     private static final String AVPROBE_NOT_INSTALLED =
93             "Program 'avprobe' is not installed on host '%1$s'";
94     private static final String REGEX_IS_VIDEO_OK =
95             "Duration: (\\d\\d:\\d\\d:\\d\\d.\\d\\d).+bitrate: (\\d+ .b\\/s)";
96     private static final String AVPROBE_STR = "avprobe";
97 
98     //===================================================================
99     // ENUMS
100     //===================================================================
101     enum HOST_SOFTWARE {
102         AVPROBE
103     }
104 
105     @Override
setDevice(ITestDevice device)106     public void setDevice(ITestDevice device) {
107         mDevice = device;
108     }
109 
110     @Override
getDevice()111     public ITestDevice getDevice() {
112         return mDevice;
113     }
114 
115     /** Main test function invoked by test harness */
116     @Override
run(ITestInvocationListener listener)117     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
118         initializeTest(listener);
119 
120         CLog.i("Verify required software is installed on host");
121         verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE.AVPROBE);
122 
123         mTestRunHelper.startTest(1);
124 
125         Map<String, String> resultsDictionary = new HashMap<String, String>();
126         try {
127             CLog.i("Verify that test options are valid");
128             if (!verifyTestParameters()) {
129                 return;
130             }
131 
132             // "resultDictionary" can be used to post results to dashboards like BlackBox
133             resultsDictionary = runTest(resultsDictionary, TEST_TIMEOUT_MS);
134             final String metricsStr = Arrays.toString(resultsDictionary.entrySet().toArray());
135             CLog.i("Uploading metrics values:\n" + metricsStr);
136             mTestRunHelper.endTest(resultsDictionary);
137         } catch (TestFailureException e) {
138             CLog.i("TestRunHelper.reportFailure triggered");
139         } finally {
140             deleteFileFromDevice(getAbsoluteFilename());
141         }
142     }
143 
144     /**
145      * Test code that calls "adb screenrecord" and checks for pass/fail criterias
146      *
147      * <p>
148      *
149      * <ul>
150      *   <li>1. Run adb screenrecord command
151      *   <li>2. Wait until there is a video file; fail if none appears
152      *   <li>3. Analyze adb output and extract recorded number of frames and video length
153      *   <li>4. Pull recorded video file off device
154      *   <li>5. Using avprobe, analyze video file and extract duration and bitrate
155      *   <li>6. Return extracted results
156      * </ul>
157      *
158      * @throws DeviceNotAvailableException
159      * @throws TestFailureException
160      */
runTest(Map<String, String> results, final long timeout)161     private Map<String, String> runTest(Map<String, String> results, final long timeout)
162             throws DeviceNotAvailableException, TestFailureException {
163         final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
164         final String cmd = generateAdbScreenRecordCommand();
165         final String deviceFileName = getAbsoluteFilename();
166 
167         CLog.i("START Execute device shell command: '" + cmd + "'");
168         getDevice().executeShellCommand(cmd, receiver, timeout, TimeUnit.MILLISECONDS, 3);
169         String adbOutput = receiver.getOutput();
170         CLog.i(adbOutput);
171         CLog.i("END Execute device shell command");
172 
173         CLog.i("Wait for recorded file: " + deviceFileName);
174         if (!waitForFile(getDevice(), timeout, deviceFileName)) {
175             mTestRunHelper.reportFailure("Recorded test file not found");
176         }
177 
178         CLog.i("Get number of recorded frames and recorded length from adb output");
179         extractVideoDataFromAdbOutput(adbOutput, results);
180 
181         CLog.i("Get duration and bitrate info from video file using '" + AVPROBE_STR + "'");
182         try {
183             extractDurationAndBitrateFromVideoFileUsingAvprobe(deviceFileName, results);
184         } catch (ParseException e) {
185             throw new RuntimeException(e);
186         }
187         deleteFileFromDevice(deviceFileName);
188         return results;
189     }
190 
191     /** Convert a string on form HH:mm:ss.SS to nearest number of seconds */
convertBitrateToKilobits(String bitrate)192     private long convertBitrateToKilobits(String bitrate) {
193         Matcher m = Pattern.compile("(\\d+) (.)b\\/s").matcher(bitrate);
194         if (!m.matches()) {
195             return -1;
196         }
197 
198         final String unit = m.group(2).toUpperCase();
199         long factor = 1;
200         switch (unit) {
201             case "K":
202                 factor = 1;
203                 break;
204             case "M":
205                 factor = 1000;
206                 break;
207             case "G":
208                 factor = 1000000;
209                 break;
210         }
211 
212         long rate = Long.parseLong(m.group(1));
213 
214         return rate * factor;
215     }
216 
217     /**
218      * Convert a string on form HH:mm:ss.SS to nearest number of seconds
219      *
220      * @throws ParseException
221      */
convertDurationToMilliseconds(String duration)222     private long convertDurationToMilliseconds(String duration) throws ParseException {
223         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS");
224         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
225         Date convertedDate = sdf.parse("1970-01-01 " + duration);
226         return convertedDate.getTime();
227     }
228 
229     /**
230      * Deletes a file off a device
231      *
232      * @param deviceFileName - path and filename to file to be deleted
233      * @throws DeviceNotAvailableException
234      */
deleteFileFromDevice(String deviceFileName)235     private void deleteFileFromDevice(String deviceFileName) throws DeviceNotAvailableException {
236         if (deviceFileName == null || deviceFileName.isEmpty()) {
237             return;
238         }
239 
240         CLog.i("Delete file from device: " + deviceFileName);
241         getDevice().executeShellCommand("rm -f " + deviceFileName);
242     }
243 
244     /**
245      * Extracts duration and bitrate data from a video file
246      *
247      * @throws DeviceNotAvailableException
248      * @throws ParseException
249      * @throws TestFailureException
250      */
extractDurationAndBitrateFromVideoFileUsingAvprobe( String deviceFileName, Map<String, String> results)251     private void extractDurationAndBitrateFromVideoFileUsingAvprobe(
252             String deviceFileName, Map<String, String> results)
253             throws DeviceNotAvailableException, ParseException, TestFailureException {
254         CLog.i("Check if the recorded file has some data in it: " + deviceFileName);
255         IFileEntry video = getDevice().getFileEntry(deviceFileName);
256         if (video == null || video.getFileEntry().getSizeValue() < 1) {
257             mTestRunHelper.reportFailure("Video Entry info failed");
258         }
259 
260         final File recordedVideo = getDevice().pullFile(deviceFileName);
261         CLog.i("Recorded video file: " + recordedVideo.getAbsolutePath());
262 
263         CommandResult result =
264                 RunUtil.getDefault()
265                         .runTimedCmd(
266                                 CMD_TIMEOUT_MS,
267                                 AVPROBE_STR,
268                                 "-loglevel",
269                                 "info",
270                                 recordedVideo.getAbsolutePath());
271 
272         // Remove file from host machine
273         FileUtil.deleteFile(recordedVideo);
274 
275         if (result.getStatus() != CommandStatus.SUCCESS) {
276             mTestRunHelper.reportFailure(AVPROBE_STR + " command failed");
277         }
278 
279         String data = result.getStderr();
280         CLog.i("data: " + data);
281         if (data == null || data.isEmpty()) {
282             mTestRunHelper.reportFailure(AVPROBE_STR + " output data is empty");
283         }
284 
285         Matcher m = Pattern.compile(REGEX_IS_VIDEO_OK).matcher(data);
286         if (!m.find()) {
287             final String errMsg =
288                     "Video verification failed; no matching verification pattern found";
289             mTestRunHelper.reportFailure(errMsg);
290         }
291 
292         String duration = m.group(1);
293         long durationInMilliseconds = convertDurationToMilliseconds(duration);
294         String bitrate = m.group(2);
295         long bitrateInKilobits = convertBitrateToKilobits(bitrate);
296 
297         results.put(RESULT_KEY_VERIFIED_DURATION, Long.toString(durationInMilliseconds / 1000));
298         results.put(RESULT_KEY_VERIFIED_BITRATE, Long.toString(bitrateInKilobits));
299     }
300 
301     /** Extracts recorded number of frames and recorded video length from adb output
302      * @throws TestFailureException */
extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results)303     private boolean extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results)
304             throws TestFailureException {
305         final String regEx = "recorded (\\d+) frames in (\\d+) second";
306         Matcher m = Pattern.compile(regEx).matcher(adbOutput);
307         if (!m.find()) {
308             mTestRunHelper.reportFailure("Regular Expression did not find recorded frames");
309             return false;
310         }
311 
312         int recordedFrames = Integer.parseInt(m.group(1));
313         int recordedLength = Integer.parseInt(m.group(2));
314         CLog.i("Recorded frames: " + recordedFrames);
315         CLog.i("Recorded length: " + recordedLength);
316         if (recordedFrames <= 0) {
317             mTestRunHelper.reportFailure("No recorded frames detected");
318             return false;
319         }
320 
321         results.put(RESULT_KEY_RECORDED_FRAMES, Integer.toString(recordedFrames));
322         results.put(RESULT_KEY_RECORDED_LENGTH, Integer.toString(recordedLength));
323         return true;
324     }
325 
326     /** Generates an adb command from passed in test options */
generateAdbScreenRecordCommand()327     private String generateAdbScreenRecordCommand() {
328         final String SPACE = " ";
329         StringBuilder sb = new StringBuilder(128);
330         sb.append("screenrecord --verbose ").append(getAbsoluteFilename());
331 
332         // Add test options if they have been passed in to the test
333         if (mRecordTimeInSeconds != -1) {
334             final long timeLimit = TimeUnit.MILLISECONDS.toSeconds(mRecordTimeInSeconds);
335             sb.append(SPACE).append(OPTION_TIME_LIMIT).append(SPACE).append(timeLimit);
336         }
337 
338         if (mVideoSize != null) {
339             sb.append(SPACE).append(OPTION_SIZE).append(SPACE).append(mVideoSize);
340         }
341 
342         if (mBitRate != -1) {
343             sb.append(SPACE).append(OPTION_BITRATE).append(SPACE).append(mBitRate);
344         }
345 
346         return sb.toString();
347     }
348 
349     /** Returns absolute path to device recorded video file */
getAbsoluteFilename()350     private String getAbsoluteFilename() {
351         return TEST_FILE;
352     }
353 
354     /** Performs test initialization steps */
initializeTest(ITestInvocationListener listener)355     private void initializeTest(ITestInvocationListener listener)
356             throws UnsupportedOperationException, DeviceNotAvailableException {
357         TestDescription testId = new TestDescription(getClass().getCanonicalName(), mRunKey);
358 
359         // Allocate helpers
360         mTestRunHelper = new TestRunHelper(listener, testId);
361 
362         getDevice().disableKeyguard();
363         getDevice().waitForDeviceAvailable(DEVICE_SYNC_MS);
364 
365         CLog.i("Sync device time to host time");
366         getDevice().setDate(new Date());
367     }
368 
369     /** Verifies that required software is installed on host machine */
verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE software)370     private void verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE software) {
371         String swName = "";
372         switch (software) {
373             case AVPROBE:
374                 swName = AVPROBE_STR;
375                 CommandResult result =
376                         RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, swName, "-version");
377                 String output = result.getStdout();
378                 if (result.getStatus() == CommandStatus.SUCCESS && output.startsWith(swName)) {
379                     return;
380                 }
381                 break;
382         }
383 
384         CLog.i("Program '" + swName + "' not found, report test failure");
385         String hostname = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, "hostname").getStdout();
386 
387         String err = String.format(AVPROBE_NOT_INSTALLED, (hostname == null) ? "" : hostname);
388         throw new RuntimeException(err);
389     }
390 
391     /** Verifies that passed in test parameters are legitimate
392      * @throws TestFailureException */
verifyTestParameters()393     private boolean verifyTestParameters() throws TestFailureException {
394         if (mRecordTimeInSeconds != -1 && mRecordTimeInSeconds < 1) {
395             final String error =
396                     String.format(ERR_OPTION_MALFORMED, OPTION_TIME_LIMIT, mRecordTimeInSeconds);
397             mTestRunHelper.reportFailure(error);
398             return false;
399         }
400 
401         if (mVideoSize != null) {
402             final String videoSizeRegEx = "\\d+x\\d+";
403             Matcher m = Pattern.compile(videoSizeRegEx).matcher(mVideoSize);
404             if (!m.matches()) {
405                 final String error = String.format(ERR_OPTION_MALFORMED, OPTION_SIZE, mVideoSize);
406                 mTestRunHelper.reportFailure(error);
407                 return false;
408             }
409         }
410 
411         if (mBitRate != -1 && mBitRate < 1) {
412             final String error = String.format(ERR_OPTION_MALFORMED, OPTION_BITRATE, mBitRate);
413             mTestRunHelper.reportFailure(error);
414             return false;
415         }
416 
417         return true;
418     }
419 
420     /** Checks for existence of a file on the device */
waitForFile( ITestDevice device, final long timeout, final String absoluteFilename)421     private static boolean waitForFile(
422             ITestDevice device, final long timeout, final String absoluteFilename)
423             throws DeviceNotAvailableException {
424         final long checkFileStartTime = System.currentTimeMillis();
425 
426         do {
427             RunUtil.getDefault().sleep(POLLING_INTERVAL_MS);
428             if (device.doesFileExist(absoluteFilename)) {
429                 return true;
430             }
431         } while (System.currentTimeMillis() - checkFileStartTime < timeout);
432 
433         return false;
434     }
435 }
436