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