1 /*
2  * Copyright (C) 2023 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 android.tools.monitors
18 
19 import android.app.Instrumentation
20 import android.media.MediaCodec
21 import android.media.MediaFormat
22 import android.media.MediaParser
23 import android.os.SystemClock
24 import android.tools.io.TraceType
25 import android.tools.traces.TRACE_CONFIG_REQUIRE_CHANGES
26 import android.tools.traces.io.ResultReader
27 import android.tools.traces.monitors.ScreenRecorder
28 import android.tools.utils.CleanFlickerEnvironmentRule
29 import android.tools.utils.newTestResultWriter
30 import androidx.test.platform.app.InstrumentationRegistry
31 import androidx.test.uiautomator.UiDevice
32 import com.google.common.truth.Truth
33 import org.junit.After
34 import org.junit.ClassRule
35 import org.junit.FixMethodOrder
36 import org.junit.Test
37 import org.junit.runners.MethodSorters
38 
39 /** Contains [ScreenRecorder] tests. To run this test: `atest FlickerLibTest:ScreenRecorderTest` */
40 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
41 class ScreenRecorderTest {
42     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
43     private val mScreenRecorder = ScreenRecorder(instrumentation.targetContext)
44 
45     @After
teardownnull46     fun teardown() {
47         if (mScreenRecorder.isEnabled) {
48             mScreenRecorder.stop(newTestResultWriter())
49         }
50     }
51 
52     @Test
videoIsRecordednull53     fun videoIsRecorded() {
54         mScreenRecorder.start()
55         val device = UiDevice.getInstance(instrumentation)
56         device.wakeUp()
57         SystemClock.sleep(500)
58         device.pressHome()
59         var remainingTime = TIMEOUT
60         do {
61             remainingTime -= 100
62             SystemClock.sleep(STEP)
63         } while (!mScreenRecorder.isFrameRecorded && remainingTime > 0)
64         val writer = newTestResultWriter()
65         mScreenRecorder.stop(writer)
66         val result = writer.write()
67 
68         val reader = ResultReader(result, TRACE_CONFIG_REQUIRE_CHANGES)
69         Truth.assertWithMessage("Screen recording file exists")
70             .that(reader.hasTraceFile(TraceType.SCREEN_RECORDING))
71             .isTrue()
72 
73         val outputData =
74             reader.readBytes(TraceType.SCREEN_RECORDING) ?: error("Screen recording not found")
75         val (metadataTrack, videoTrack) = parseScreenRecording(outputData)
76 
77         Truth.assertThat(metadataTrack.isEmpty()).isFalse()
78         Truth.assertThat(videoTrack.isEmpty()).isFalse()
79 
80         val actualMagicString = metadataTrack.copyOfRange(0, WINSCOPE_MAGIC_STRING.size)
81         Truth.assertThat(actualMagicString).isEqualTo(WINSCOPE_MAGIC_STRING)
82     }
83 
parseScreenRecordingnull84     private fun parseScreenRecording(data: ByteArray): Pair<ByteArray, ByteArray> {
85         val inputReader = ScreenRecorderSeekableInputReader(data)
86         val outputConsumer = ScreenRecorderOutputConsumer()
87         val mediaParser = MediaParser.create(outputConsumer)
88 
89         while (mediaParser.advance(inputReader)) {
90             // no op
91         }
92         mediaParser.release()
93 
94         return Pair(outputConsumer.getMetadataTrack(), outputConsumer.getVideoTrack())
95     }
96 
97     companion object {
98         private const val TIMEOUT = 10000L
99         private const val STEP = 100L
100         private val WINSCOPE_MAGIC_STRING =
101             byteArrayOf(
102                 0x23,
103                 0x56,
104                 0x56,
105                 0x31,
106                 0x4e,
107                 0x53,
108                 0x43,
109                 0x30,
110                 0x50,
111                 0x45,
112                 0x54,
113                 0x31,
114                 0x4d,
115                 0x45,
116                 0x32,
117                 0x23
118             ) // "#VV1NSC0PET1ME2#"
119 
120         @ClassRule @JvmField val ENV_CLEANUP = CleanFlickerEnvironmentRule()
121     }
122 
123     internal class ScreenRecorderSeekableInputReader(private val bytes: ByteArray) :
124         MediaParser.SeekableInputReader {
125         private var position = 0L
126 
getPositionnull127         override fun getPosition(): Long = position
128 
129         override fun getLength(): Long = bytes.size.toLong() - position
130 
131         override fun seekToPosition(position: Long) {
132             this.position = position
133         }
134 
readnull135         override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
136             if (position >= bytes.size) {
137                 return -1
138             }
139 
140             val actualLength = kotlin.math.min(readLength.toLong(), bytes.size - position)
141             for (i in 0 until actualLength) {
142                 buffer[(offset + i).toInt()] = bytes[(position + i).toInt()]
143             }
144 
145             position += actualLength
146 
147             return actualLength.toInt()
148         }
149     }
150 
151     internal class ScreenRecorderOutputConsumer : MediaParser.OutputConsumer {
152         private var videoTrack = ArrayList<Byte>()
153         private var metadataTrack = ArrayList<Byte>()
154         private var videoTrackIndex = -1
155         private var metadataTrackIndex = -1
156         private val auxBuffer = ByteArray(4 * 1024)
157 
getVideoTracknull158         fun getVideoTrack(): ByteArray {
159             return videoTrack.toByteArray()
160         }
161 
getMetadataTracknull162         fun getMetadataTrack(): ByteArray {
163             return metadataTrack.toByteArray()
164         }
165 
onSeekMapFoundnull166         override fun onSeekMapFound(seekMap: MediaParser.SeekMap) {
167             // do nothing
168         }
169 
onTrackCountFoundnull170         override fun onTrackCountFound(numberOfTracks: Int) {
171             Truth.assertThat(numberOfTracks).isEqualTo(2)
172         }
173 
onTrackDataFoundnull174         override fun onTrackDataFound(i: Int, trackData: MediaParser.TrackData) {
175             if (
176                 videoTrackIndex == -1 &&
177                     trackData.mediaFormat.getString(MediaFormat.KEY_MIME, "").startsWith("video/")
178             ) {
179                 videoTrackIndex = i
180             }
181 
182             if (
183                 metadataTrackIndex == -1 &&
184                     trackData.mediaFormat.getString(MediaFormat.KEY_MIME, "") ==
185                         "application/octet-stream"
186             ) {
187                 metadataTrackIndex = i
188             }
189         }
190 
onSampleDataFoundnull191         override fun onSampleDataFound(trackIndex: Int, inputReader: MediaParser.InputReader) {
192             when (trackIndex) {
193                 videoTrackIndex -> processSampleData(inputReader, videoTrack)
194                 metadataTrackIndex -> processSampleData(inputReader, metadataTrack)
195                 else -> throw RuntimeException("unexpected track index: $trackIndex")
196             }
197         }
198 
onSampleCompletednull199         override fun onSampleCompleted(
200             trackIndex: Int,
201             timeMicros: Long,
202             flags: Int,
203             size: Int,
204             offset: Int,
205             cryptoData: MediaCodec.CryptoInfo?
206         ) {
207             // do nothing
208         }
209 
processSampleDatanull210         private fun processSampleData(
211             inputReader: MediaParser.InputReader,
212             buffer: ArrayList<Byte>
213         ) {
214             while (inputReader.length > 0) {
215                 val requestLength = kotlin.math.min(inputReader.length, auxBuffer.size.toLong())
216                 val actualLength = inputReader.read(auxBuffer, 0, requestLength.toInt())
217                 if (actualLength == -1) {
218                     break
219                 }
220 
221                 for (i in 0 until actualLength) {
222                     buffer.add(auxBuffer[i])
223                 }
224             }
225         }
226     }
227 }
228