1 /*
2  * Copyright 2021 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 androidx.compose.ui.tooling
18 
19 import androidx.compose.ui.tooling.data.Group
20 import androidx.compose.ui.tooling.data.UiToolingDataApi
21 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
22 import kotlin.collections.removeLast as removeLastKt
23 
24 /** Tries to find the [Class] of the [PreviewParameterProvider] corresponding to the given FQN. */
asPreviewProviderClassnull25 internal fun String.asPreviewProviderClass(): Class<out PreviewParameterProvider<*>>? {
26     try {
27         @Suppress("UNCHECKED_CAST")
28         return Class.forName(this) as? Class<out PreviewParameterProvider<*>>
29     } catch (e: ClassNotFoundException) {
30         PreviewLogger.logError("Unable to find PreviewProvider '$this'", e)
31         return null
32     }
33 }
34 
35 /**
36  * Returns an array with some values of a [PreviewParameterProvider]. If the given provider class is
37  * `null`, returns an empty array. Otherwise, if the given `parameterProviderIndex` is a valid
38  * index, returns a single-element array containing the value corresponding to that particular index
39  * in the provider's sequence. Finally, returns an array with all the values of the provider's
40  * sequence if `parameterProviderIndex` is invalid, e.g. negative.
41  */
getPreviewProviderParametersnull42 internal fun getPreviewProviderParameters(
43     parameterProviderClass: Class<out PreviewParameterProvider<*>>?,
44     parameterProviderIndex: Int
45 ): Array<Any?> {
46     if (parameterProviderClass != null) {
47         try {
48             val constructor =
49                 parameterProviderClass.constructors
50                     .singleOrNull { it.parameterTypes.isEmpty() }
51                     ?.apply { isAccessible = true }
52                     ?: throw IllegalArgumentException(
53                         "PreviewParameterProvider constructor can not" + " have parameters"
54                     )
55             val params = constructor.newInstance() as PreviewParameterProvider<*>
56             if (parameterProviderIndex < 0) {
57                 return params.values.toArray(params.count)
58             }
59             return listOf(params.values.elementAt(parameterProviderIndex))
60                 .map { unwrapIfInline(it) }
61                 .toTypedArray()
62         } catch (e: KotlinReflectionNotSupportedError) {
63             // kotlin-reflect runtime dependency not found. Suggest adding it.
64             throw IllegalStateException(
65                 "Deploying Compose Previews with PreviewParameterProvider " +
66                     "arguments requires adding a dependency to the kotlin-reflect library.\n" +
67                     "Consider adding 'debugImplementation " +
68                     "\"org.jetbrains.kotlin:kotlin-reflect:\$kotlin_version\"' " +
69                     "to the module's build.gradle."
70             )
71         }
72     } else {
73         return emptyArray()
74     }
75 }
76 
77 /**
78  * Checks if the object is of inlined value type. If yes, unwraps and returns the packed value If
79  * not, returns the object as it is
80  */
unwrapIfInlinenull81 private fun unwrapIfInline(classToCheck: Any?): Any? {
82     // At the moment is not possible to use classToCheck::class.isValue, even if it works when
83     // running tests, is not working once trying to run the Preview instead.
84     // it would be possible in the future.
85     // see also https://kotlinlang.org/docs/inline-classes.html
86     if (classToCheck != null && classToCheck::class.java.annotations.any { it is JvmInline }) {
87         // The first primitive declared field in the class is the value wrapped
88         val fieldName: String =
89             classToCheck::class.java.declaredFields.first { it.type.isPrimitive }.name
90         return classToCheck::class
91             .java
92             .getDeclaredField(fieldName)
93             .also { it.isAccessible = true }
94             .get(classToCheck)
95     }
96     return classToCheck
97 }
98 
99 @OptIn(UiToolingDataApi::class)
firstOrNullnull100 internal fun Group.firstOrNull(predicate: (Group) -> Boolean): Group? {
101     return findGroupsThatMatchPredicate(this, predicate, true).firstOrNull()
102 }
103 
104 @OptIn(UiToolingDataApi::class)
findAllnull105 internal fun Group.findAll(predicate: (Group) -> Boolean): List<Group> {
106     return findGroupsThatMatchPredicate(this, predicate)
107 }
108 
109 /**
110  * Search [Group]s that match a given [predicate], starting from a given [root]. An optional boolean
111  * parameter can be set if we're interested in a single occurrence. If it's set, we return early
112  * after finding the first matching [Group].
113  */
114 @OptIn(UiToolingDataApi::class)
findGroupsThatMatchPredicatenull115 private fun findGroupsThatMatchPredicate(
116     root: Group,
117     predicate: (Group) -> Boolean,
118     findOnlyFirst: Boolean = false
119 ): List<Group> {
120     val result = mutableListOf<Group>()
121     val stack = mutableListOf(root)
122     while (stack.isNotEmpty()) {
123         val current = stack.removeLastKt()
124         if (predicate(current)) {
125             if (findOnlyFirst) {
126                 return listOf(current)
127             }
128             result.add(current)
129         }
130         stack.addAll(current.children)
131     }
132     return result
133 }
134 
toArraynull135 private fun Sequence<Any?>.toArray(size: Int): Array<Any?> {
136     val iterator = iterator()
137     return Array(size) { iterator.next() }
138 }
139 
140 /** A simple wrapper to store and throw exception later in a thread-safe way. */
141 internal class ThreadSafeException {
142     private var exception: Throwable? = null
143 
144     /** A lock to take to access exception. */
145     private val lock = Any()
146 
setnull147     fun set(throwable: Throwable) {
148         synchronized(lock) { exception = throwable }
149     }
150 
throwIfPresentnull151     fun throwIfPresent() {
152         synchronized(lock) {
153             exception?.let {
154                 exception = null
155                 throw it
156             }
157         }
158     }
159 }
160