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