1 /*
2  * Copyright (C) 2022 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 
17 package com.android.pixel.utils;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.os.SystemClock;
24 import android.support.test.uiautomator.By;
25 import android.support.test.uiautomator.UiDevice;
26 import android.util.Log;
27 
28 import com.google.common.base.Preconditions;
29 
30 import org.junit.Assert;
31 
32 import java.io.File;
33 import java.io.IOException;
34 import java.nio.file.Paths;
35 import java.util.Optional;
36 
37 public class DeviceUtils {
38     private static final String TAG = DeviceUtils.class.getSimpleName();
39     private static final String LOG_DATA_DIR = "/sdcard/logData";
40     private static final int MAX_RECORDING_PARTS = 5;
41     private static final long WAIT_ONE_SECOND_IN_MS = 1000;
42     private static final long VIDEO_TAIL_BUFFER = 500;
43     private static final String DISMISS_KEYGUARD = "wm dismiss-keyguard";
44 
45     private String mFolderDir = LOG_DATA_DIR;
46     private String mTestName = TAG;
47     private RecordingThread mCurrentThread;
48     private File mLogDataDir;
49     private UiDevice mDevice;
50 
DeviceUtils(UiDevice device)51     public DeviceUtils(UiDevice device) {
52         mDevice = device;
53     }
54 
55     /**
56      * Sets the test name and the folder path for the current test.
57      *
58      * @param testName The test name.
59      */
setTestName(String testName)60     public void setTestName(String testName) {
61         Optional<String> optionalTestName = Optional.ofNullable(testName);
62         if (optionalTestName.isPresent()) {
63             mTestName = optionalTestName.get();
64             mFolderDir = String.join("/", LOG_DATA_DIR, optionalTestName.get());
65         } else {
66             Preconditions.checkNotNull(testName, "testName cannot be null");
67         }
68     }
69 
70     /** Create a directory to save test screenshots, screenrecord and text files. */
createLogDataDir()71     public void createLogDataDir() {
72         mLogDataDir = new File(mFolderDir);
73         if (mLogDataDir.exists()) {
74             String[] children = mLogDataDir.list();
75             for (String file : children) {
76                 new File(mLogDataDir, file).delete();
77             }
78         } else {
79             mLogDataDir.mkdirs();
80         }
81     }
82 
83     /** Wake up the device and dismiss the keyguard. */
wakeAndUnlockScreen()84     public void wakeAndUnlockScreen() throws Exception {
85         mDevice.wakeUp();
86         SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
87         mDevice.executeShellCommand(DISMISS_KEYGUARD);
88         SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
89     }
90 
91     /**
92      * Go back to home screen by pressing back key five times and home key to avoid the infinite
93      * loop since some apps' activities cannot be exited to home screen by back key event.
94      */
backToHome(String launcherPkg)95     public void backToHome(String launcherPkg) {
96         for (int i = 0; i < 5; i++) {
97             mDevice.pressBack();
98             mDevice.waitForIdle();
99             if (mDevice.hasObject(By.pkg(launcherPkg))) {
100                 break;
101             }
102         }
103         mDevice.pressHome();
104     }
105 
106     /**
107      * Launch an app with the given package name
108      *
109      * @param packageName Name of package to be launched
110      */
launchApp(String packageName)111     public void launchApp(String packageName) {
112         Context context = getInstrumentation().getContext();
113         Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
114         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
115         context.startActivity(intent);
116     }
117 
118     /**
119      * Take a screenshot on the device and save it in {@code logDataDir}.
120      *
121      * @param packageName The package name of 3P apps screenshotted.
122      * @param description The description of actions or operations on the device.
123      */
takeScreenshot(String packageName, String description)124     public void takeScreenshot(String packageName, String description) {
125         File screenshot =
126                 new File(
127                         mFolderDir,
128                         String.format(
129                                 "%s_%s_screenshot_%s.png", mTestName, packageName, description));
130         mDevice.takeScreenshot(screenshot);
131     }
132 
133     /**
134      * Start the screen recording.
135      *
136      * @param packageName The package name of 3P apps screenrecorded.
137      */
startRecording(String packageName)138     public void startRecording(String packageName) {
139         Log.v(TAG, "Started Recording");
140         mCurrentThread =
141                 new RecordingThread(
142                         "test-screen-record",
143                         String.format("%s_%s_screenrecord", mTestName, packageName));
144         mCurrentThread.start();
145     }
146 
147     /** Stop already started screen recording. */
stopRecording()148     public void stopRecording() {
149         // Skip if not directory.
150         if (mLogDataDir == null) {
151             return;
152         }
153         // Add some extra time to the video end.
154         SystemClock.sleep(VIDEO_TAIL_BUFFER);
155         // Ctrl + C all screen record processes.
156         mCurrentThread.cancel();
157         // Wait for the thread to completely die.
158         try {
159             mCurrentThread.join();
160         } catch (InterruptedException ex) {
161             Log.e(TAG, "Interrupted when joining the recording thread.", ex);
162         }
163         Log.v(TAG, "Stopped Recording");
164     }
165 
166     /** Returns the recording's name for {@code part} of launch description. */
getOutputFile(String description, int part)167     public File getOutputFile(String description, int part) {
168         // Omit the iteration number for the first iteration.
169         final String fileName = String.format("%s-video%s.mp4", description, part == 1 ? "" : part);
170         return Paths.get(mLogDataDir.getAbsolutePath(), fileName).toFile();
171     }
172 
173     /**
174      * Encapsulates the start and stop screen recording logic. Copied from ScreenRecordCollector.
175      */
176     private class RecordingThread extends Thread {
177         private final String mDescription;
178 
179         private boolean mContinue;
180 
RecordingThread(String name, String description)181         RecordingThread(String name, String description) {
182             super(name);
183 
184             mContinue = true;
185 
186             Assert.assertNotNull("No test description provided for recording.", description);
187             mDescription = description;
188         }
189 
190         @Override
run()191         public void run() {
192             try {
193                 // Start at i = 1 to encode parts as X.mp4, X2.mp4, X3.mp4, etc.
194                 for (int i = 1; i <= MAX_RECORDING_PARTS && mContinue; i++) {
195                     File output = getOutputFile(mDescription, i);
196                     Log.d(TAG, String.format("Recording screen to %s", output.getAbsolutePath()));
197                     // Make sure not to block on this background command in the main thread so
198                     // that the test continues to run, but block in this thread so it does not
199                     // trigger a new screen recording session before the prior one completes.
200                     mDevice.executeShellCommand(
201                             String.format("screenrecord %s", output.getAbsolutePath()));
202                 }
203             } catch (IOException e) {
204                 throw new RuntimeException("Caught exception while screen recording.");
205             }
206         }
207 
cancel()208         public void cancel() {
209             mContinue = false;
210             // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
211             try {
212                 String[] pids = mDevice.executeShellCommand("pidof screenrecord").split(" ");
213                 for (String pid : pids) {
214                     // Avoid empty process ids, because of weird splitting behavior.
215                     if (pid.isEmpty()) {
216                         continue;
217                     }
218                     mDevice.executeShellCommand(String.format("kill -2 %s", pid));
219                     Log.d(TAG, String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
220                 }
221             } catch (IOException e) {
222                 throw new RuntimeException("Failed to kill screen recording process.");
223             }
224         }
225     }
226 }
227