1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.launcher3.util.rule
17 
18 import android.app.Activity
19 import android.app.Application
20 import android.media.permission.SafeCloseable
21 import android.os.Bundle
22 import androidx.test.core.app.ApplicationProvider
23 import androidx.test.platform.app.InstrumentationRegistry
24 import com.android.app.viewcapture.SimpleViewCapture
25 import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
26 import com.android.app.viewcapture.data.ExportedData
27 import com.android.launcher3.tapl.TestHelpers
28 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
29 import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
30 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
31 import java.io.BufferedOutputStream
32 import java.io.FileOutputStream
33 import java.io.IOException
34 import java.io.OutputStreamWriter
35 import java.util.function.Supplier
36 import org.junit.Assert.assertTrue
37 import org.junit.Assert.fail
38 import org.junit.rules.TestRule
39 import org.junit.runner.Description
40 import org.junit.runners.model.Statement
41 
42 /**
43  * This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture
44  * instance that other test rules use to dump the timelapse hierarchy upon an error during a test.
45  *
46  * This rule will not work in OOP tests that don't have access to the activity under test.
47  */
48 class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : TestRule {
49     private val viewCapture = SimpleViewCapture("test-view-capture")
50     var viewCaptureData: ExportedData? = null
51         private set
52 
53     override fun apply(base: Statement, description: Description): Statement {
54         // Skip view capture collection in Launcher3 tests to avoid hidden API check exception.
55         if (
56             "com.android.launcher3.tests" ==
57                 InstrumentationRegistry.getInstrumentation().context.packageName
58         )
59             return base
60 
61         return object : Statement() {
62             override fun evaluate() {
63                 viewCaptureData = null
64                 val windowListenerCloseables = mutableListOf<SafeCloseable>()
65 
66                 startCapturingExistingActivity(windowListenerCloseables)
67 
68                 val lifecycleCallbacks =
69                     object : ActivityLifecycleCallbacksAdapter {
70                         override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
71                             startCapture(windowListenerCloseables, activity)
72                         }
73 
74                         override fun onActivityDestroyed(activity: Activity) {
75                             viewCapture.stopCapture(activity.window.decorView)
76                         }
77                     }
78 
79                 val application = ApplicationProvider.getApplicationContext<Application>()
80                 application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
81 
82                 try {
83                     base.evaluate()
84                 } finally {
85                     application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
86 
87                     viewCaptureData =
88                         viewCapture.getExportedData(ApplicationProvider.getApplicationContext())
89 
90                     // Clean up ViewCapture references here rather than in onActivityDestroyed so
91                     // test code can access view hierarchy capture. onActivityDestroyed would delete
92                     // view capture data before FailureWatcher could output it as a test artifact.
93                     // This is on the main thread to avoid a race condition where the onDrawListener
94                     // is removed while onDraw is running, resulting in an IllegalStateException.
95                     MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
96                 }
97 
98                 analyzeViewCapture(description)
99             }
100 
101             private fun startCapturingExistingActivity(
102                 windowListenerCloseables: MutableCollection<SafeCloseable>
103             ) {
104                 val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
105                 if (alreadyOpenActivity != null) {
106                     startCapture(windowListenerCloseables, alreadyOpenActivity)
107                 }
108             }
109 
110             private fun startCapture(
111                 windowListenerCloseables: MutableCollection<SafeCloseable>,
112                 activity: Activity
113             ) {
114                 windowListenerCloseables.add(
115                     viewCapture.startCapture(
116                         activity.window.decorView,
117                         "${description.testClass?.simpleName}.${description.methodName}"
118                     )
119                 )
120             }
121         }
122     }
123 
124     private fun analyzeViewCapture(description: Description) {
125         // OOP tests don't produce ViewCapture data
126         if (!TestHelpers.isInLauncherProcess()) return
127 
128         // Due to flakiness of ViewCapture verifier, don't run the check in presubmit
129         if (TestStabilityRule.getRunFlavor() != PLATFORM_POSTSUBMIT) return
130 
131         var frameCount = 0
132         for (i in 0 until viewCaptureData!!.windowDataCount) {
133             frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
134         }
135 
136         val mayProduceNoFrames = description.getAnnotation(MayProduceNoFrames::class.java) != null
137         assertTrue("Empty ViewCapture data", mayProduceNoFrames || frameCount > 0)
138 
139         val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
140         if (!anomalies.isEmpty()) {
141             val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
142             try {
143                 OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
144                     writer.write("View animation anomalies detected.\r\n")
145                     writer.write(
146                         "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
147                     )
148                     writer.write("List of views with animation anomalies:\r\n")
149 
150                     for ((viewPath, message) in anomalies) {
151                         writer.write("View: $viewPath\r\n        $message\r\n")
152                     }
153                 }
154             } catch (ex: IOException) {
155                 throw RuntimeException(ex)
156             }
157 
158             val (viewPath, message) = anomalies.entries.first()
159             fail(
160                 "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
161             )
162         }
163     }
164 
165     @Retention(AnnotationRetention.RUNTIME)
166     @Target(AnnotationTarget.FUNCTION)
167     annotation class MayProduceNoFrames
168 }
169