blob: 5af453d526e41ebeb06421670d4fbbee45d69c2d [file] [log] [blame]
Winson9947f1e2019-08-16 10:20:39 -07001/*
2 * Copyright (C) 2019 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
17package android.content.res.loader.test
18
19import android.content.Context
20import android.content.res.AssetManager
21import android.content.res.Configuration
22import android.content.res.Resources
23import android.content.res.loader.ResourceLoader
24import android.content.res.loader.ResourcesProvider
25import android.os.ParcelFileDescriptor
26import android.util.TypedValue
27import androidx.annotation.DimenRes
28import androidx.annotation.DrawableRes
29import androidx.annotation.LayoutRes
30import androidx.annotation.StringRes
31import androidx.test.InstrumentationRegistry
32import org.junit.After
33import org.junit.Before
34import org.mockito.ArgumentMatchers
35import org.mockito.ArgumentMatchers.anyInt
36import org.mockito.ArgumentMatchers.argThat
37import org.mockito.ArgumentMatchers.eq
38import org.mockito.Mockito.doAnswer
39import org.mockito.Mockito.doReturn
40import org.mockito.Mockito.mock
41import java.io.Closeable
42
43abstract class ResourceLoaderTestBase {
44
45 open lateinit var dataType: DataType
46
47 protected lateinit var context: Context
48 protected open val resources: Resources
49 get() = context.resources
50 protected open val assets: AssetManager
51 get() = resources.assets
52
53 // Track opened streams and ResourcesProviders to close them after testing
54 private val openedObjects = mutableListOf<Closeable>()
55
56 @Before
57 fun setUpBase() {
58 context = InstrumentationRegistry.getTargetContext()
59 }
60
61 @After
62 fun removeAllLoaders() {
63 resources.setLoaders(null)
64 openedObjects.forEach {
65 try {
66 it.close()
67 } catch (ignored: Exception) {
68 }
69 }
70 }
71
72 protected fun getString(@StringRes stringRes: Int, debugLog: Boolean = false) =
73 logResolution(debugLog) { getString(stringRes) }
74
75 protected fun getDrawable(@DrawableRes drawableRes: Int, debugLog: Boolean = false) =
76 logResolution(debugLog) { getDrawable(drawableRes) }
77
78 protected fun getLayout(@LayoutRes layoutRes: Int, debugLog: Boolean = false) =
79 logResolution(debugLog) { getLayout(layoutRes) }
80
81 protected fun getDimensionPixelSize(@DimenRes dimenRes: Int, debugLog: Boolean = false) =
82 logResolution(debugLog) { getDimensionPixelSize(dimenRes) }
83
84 private fun <T> logResolution(debugLog: Boolean = false, block: Resources.() -> T): T {
85 if (debugLog) {
86 resources.assets.setResourceResolutionLoggingEnabled(true)
87 }
88
89 var thrown = false
90
91 try {
92 return resources.block()
93 } catch (t: Throwable) {
94 // No good way to log to test output other than throwing an exception
95 if (debugLog) {
96 thrown = true
97 throw IllegalStateException(resources.assets.lastResourceResolution, t)
98 } else {
99 throw t
100 }
101 } finally {
102 if (!thrown && debugLog) {
103 throw IllegalStateException(resources.assets.lastResourceResolution)
104 }
105 }
106 }
107
108 protected fun updateConfiguration(block: Configuration.() -> Unit) {
109 val configuration = Configuration().apply {
110 setTo(resources.configuration)
111 block()
112 }
113
114 resources.updateConfiguration(configuration, resources.displayMetrics)
115 }
116
117 protected fun String.openLoader(
118 dataType: DataType = this@ResourceLoaderTestBase.dataType
119 ): Pair<ResourceLoader, ResourcesProvider> = when (dataType) {
120 DataType.APK -> {
121 mock(ResourceLoader::class.java) to context.copiedRawFile("${this}Apk").use {
122 ResourcesProvider.loadFromApk(it)
123 }.also { openedObjects += it }
124 }
125 DataType.ARSC -> {
126 mock(ResourceLoader::class.java) to openArsc(this)
127 }
128 DataType.SPLIT -> {
129 mock(ResourceLoader::class.java) to ResourcesProvider.loadFromSplit(context, this)
130 }
131 DataType.ASSET -> mockLoader {
132 doAnswer { byteInputStream() }.`when`(it)
133 .loadAsset(eq("assets/Asset.txt"), anyInt())
134 }
135 DataType.ASSET_FD -> mockLoader {
136 doAnswer {
137 val file = context.filesDir.resolve("Asset.txt")
138 file.writeText(this)
139 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
140 }.`when`(it).loadAssetFd("assets/Asset.txt")
141 }
142 DataType.NON_ASSET -> mockLoader {
143 doAnswer {
144 val file = context.filesDir.resolve("NonAsset.txt")
145 file.writeText(this)
146 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
147 }.`when`(it).loadAssetFd("NonAsset.txt")
148 }
149 DataType.NON_ASSET_DRAWABLE -> mockLoader(openArsc(this)) {
150 doReturn(null).`when`(it).loadDrawable(argThat { value ->
151 value.type == TypedValue.TYPE_STRING &&
152 value.resourceId == 0x7f010001 &&
153 value.string == "res/drawable-nodpi-v4/non_asset_drawable.xml"
154 }, eq(0x7f010001), anyInt(), ArgumentMatchers.any())
155
156 doAnswer { context.copiedRawFile(this) }.`when`(it)
157 .loadAssetFd("res/drawable-nodpi-v4/non_asset_drawable.xml")
158 }
159 DataType.NON_ASSET_BITMAP -> mockLoader(openArsc(this)) {
160 doReturn(null).`when`(it).loadDrawable(argThat { value ->
161 value.type == TypedValue.TYPE_STRING &&
162 value.resourceId == 0x7f010000 &&
163 value.string == "res/drawable-nodpi-v4/non_asset_bitmap.png"
164 }, eq(0x7f010000), anyInt(), ArgumentMatchers.any())
165
166 doAnswer { resources.openRawResourceFd(rawFile(this)).createInputStream() }
167 .`when`(it)
168 .loadAsset(eq("res/drawable-nodpi-v4/non_asset_bitmap.png"), anyInt())
169 }
170 DataType.NON_ASSET_LAYOUT -> mockLoader(openArsc(this)) {
171 doReturn(null).`when`(it)
172 .loadXmlResourceParser("res/layout/layout.xml", 0x7f020000)
173
174 doAnswer { context.copiedRawFile(this) }.`when`(it)
175 .loadAssetFd("res/layout/layout.xml")
176 }
177 }
178
179 protected fun mockLoader(
180 provider: ResourcesProvider = ResourcesProvider.empty(),
181 block: (ResourceLoader) -> Unit = {}
182 ): Pair<ResourceLoader, ResourcesProvider> {
183 return mock(ResourceLoader::class.java, Utils.ANSWER_THROWS)
184 .apply(block) to provider
185 }
186
187 protected fun openArsc(rawName: String): ResourcesProvider {
188 return context.copiedRawFile("${rawName}Arsc")
189 .use { ResourcesProvider.loadFromArsc(it) }
190 .also { openedObjects += it }
191 }
192
193 // This specifically uses addLoader so both behaviors are tested
194 protected fun addLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
195 pairs.forEach { resources.addLoader(it.first, it.second) }
196 }
197
198 protected fun setLoaders(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
199 resources.setLoaders(pairs.map { android.util.Pair(it.first, it.second) })
200 }
201
202 protected fun addLoader(pair: Pair<out ResourceLoader, ResourcesProvider>, index: Int) {
203 resources.addLoader(pair.first, pair.second, index)
204 }
205
206 protected fun removeLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
207 pairs.forEach { resources.removeLoader(it.first) }
208 }
209
210 protected fun getLoaders(): MutableList<Pair<ResourceLoader, ResourcesProvider>> {
211 // Cast instead of toMutableList to maintain the same object
212 return resources.getLoaders() as MutableList<Pair<ResourceLoader, ResourcesProvider>>
213 }
214
215 enum class DataType {
216 APK,
217 ARSC,
218 SPLIT,
219 ASSET,
220 ASSET_FD,
221 NON_ASSET,
222 NON_ASSET_DRAWABLE,
223 NON_ASSET_BITMAP,
224 NON_ASSET_LAYOUT,
225 }
226}