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