expo-updates 0.28.2 → 0.28.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/android/build.gradle +11 -6
  3. package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +4 -5
  4. package/android/src/main/java/expo/modules/updates/db/dao/AssetDao.kt +16 -10
  5. package/android/src/main/java/expo/modules/updates/db/entity/AssetEntity.kt +9 -0
  6. package/android/src/main/java/expo/modules/updates/launcher/DatabaseLauncher.kt +42 -5
  7. package/android/src/main/java/expo/modules/updates/loader/EmbeddedLoader.kt +21 -9
  8. package/android/src/main/java/expo/modules/updates/loader/Loader.kt +2 -6
  9. package/android/src/main/java/expo/modules/updates/loader/LoaderFiles.kt +7 -2
  10. package/android/src/main/java/expo/modules/updates/utils/AndroidResourceAssetUtils.kt +120 -0
  11. package/bin/check-for-changed-paths/index.js +4 -0
  12. package/build/ExpoUpdates.web.d.ts.map +1 -1
  13. package/build/ExpoUpdates.web.js +1 -1
  14. package/build/ExpoUpdates.web.js.map +1 -1
  15. package/e2e/fixtures/Updates.e2e.ts +143 -113
  16. package/e2e/fixtures/project_files/scripts/check-android-emulator.ts +44 -0
  17. package/e2e/setup/check-for-changed-paths.ts +128 -0
  18. package/e2e/setup/create-eas-project-tv.ts +5 -2
  19. package/e2e/setup/create-eas-project.ts +5 -2
  20. package/e2e/setup/paths-filter/LICENSE +22 -0
  21. package/e2e/setup/paths-filter/README.md +567 -0
  22. package/e2e/setup/paths-filter/file.ts +13 -0
  23. package/e2e/setup/paths-filter/filter.ts +164 -0
  24. package/e2e/setup/paths-filter/git.ts +313 -0
  25. package/e2e/setup/paths-filter/paths-filter-dependencies.ts +32 -0
  26. package/e2e/setup/project.ts +1 -0
  27. package/e2e/tsconfig.json +4 -0
  28. package/package.json +13 -7
  29. package/src/ExpoUpdates.web.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 0.28.4 — 2025-04-14
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android] Added `EX_UPDATES_COPY_EMBEDDED_ASSETS` flag which is false by default, to not copy embedded assets. ([#36059](https://github.com/expo/expo/pull/36059) by [@kudo](https://github.com/kudo))
18
+
19
+ ## 0.28.3 — 2025-04-11
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 0.28.2 — 2025-04-09
14
24
 
15
25
  _This version does not introduce any user-facing changes._
@@ -39,7 +39,7 @@ expoModule {
39
39
  }
40
40
 
41
41
  group = 'host.exp.exponent'
42
- version = '0.28.2'
42
+ version = '0.28.4'
43
43
 
44
44
  // Utility method to derive boolean values from the environment or from Java properties,
45
45
  // and return them as strings to be used in BuildConfig fields
@@ -72,6 +72,9 @@ def exUpdatesCustomInit = getBoolStringFromPropOrEnv("EX_UPDATES_CUSTOM_INIT", f
72
72
  // (default true)
73
73
  def exUpdatesAndroidDelayLoadApp = getBoolStringFromPropOrEnv("EX_UPDATES_ANDROID_DELAY_LOAD_APP", true)
74
74
 
75
+ // If true, updates will copy embedded assets to file system when startup. (default false)
76
+ def exUpdatesCopyEmbeddedAssets = getBoolStringFromPropOrEnv("EX_UPDATES_COPY_EMBEDDED_ASSETS", false)
77
+
75
78
  def useDevClient = findProject(":expo-dev-client") != null
76
79
 
77
80
  android {
@@ -82,13 +85,14 @@ android {
82
85
  namespace "expo.modules.updates"
83
86
  defaultConfig {
84
87
  versionCode 31
85
- versionName '0.28.2'
88
+ versionName '0.28.4'
86
89
  consumerProguardFiles("proguard-rules.pro")
87
90
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
88
91
 
89
92
  buildConfigField("boolean", "EX_UPDATES_NATIVE_DEBUG", exUpdatesNativeDebug)
90
93
  buildConfigField("boolean", "EX_UPDATES_CUSTOM_INIT", exUpdatesCustomInit)
91
94
  buildConfigField("boolean", "EX_UPDATES_ANDROID_DELAY_LOAD_APP", exUpdatesAndroidDelayLoadApp)
95
+ buildConfigField("boolean", "EX_UPDATES_COPY_EMBEDDED_ASSETS", exUpdatesCopyEmbeddedAssets)
92
96
  buildConfigField("boolean", "USE_DEV_CLIENT", useDevClient.toString())
93
97
  }
94
98
  testOptions {
@@ -136,16 +140,17 @@ dependencies {
136
140
  implementation("org.bouncycastle:bcutil-jdk15to18:1.78.1")
137
141
 
138
142
  testImplementation 'junit:junit:4.13.2'
139
- testImplementation 'androidx.test:core:1.5.0'
143
+ testImplementation 'androidx.test:core:1.6.1'
144
+ testImplementation 'com.google.truth:truth:1.1.2'
140
145
  testImplementation "io.mockk:mockk:$mockk_version"
141
146
  testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}"
142
147
  testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
143
148
  testImplementation 'org.robolectric:robolectric:4.14.1'
144
149
 
145
150
  androidTestImplementation 'com.squareup.okio:okio:2.9.0'
146
- androidTestImplementation 'androidx.test:runner:1.5.2'
147
- androidTestImplementation 'androidx.test:core:1.5.0'
148
- androidTestImplementation 'androidx.test:rules:1.5.0'
151
+ androidTestImplementation 'androidx.test:runner:1.6.2'
152
+ androidTestImplementation 'androidx.test:core:1.6.1'
153
+ androidTestImplementation 'androidx.test:rules:1.6.1'
149
154
  androidTestImplementation "io.mockk:mockk-android:$mockk_version"
150
155
  androidTestImplementation "androidx.room:room-testing:$room_version"
151
156
  androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
@@ -19,7 +19,6 @@ import java.text.ParseException
19
19
  import java.text.SimpleDateFormat
20
20
  import java.util.*
21
21
  import java.util.regex.Pattern
22
- import kotlin.experimental.and
23
22
 
24
23
  /**
25
24
  * Miscellaneous helper functions that are used by multiple classes in the library.
@@ -145,11 +144,11 @@ object UpdatesUtils {
145
144
  }
146
145
  }
147
146
 
147
+ /**
148
+ * Create an asset filename in file system (files are saved in the `.expo-internal` directory)
149
+ */
148
150
  fun createFilenameForAsset(asset: AssetEntity): String {
149
- var fileExtension: String? = ""
150
- if (asset.type != null) {
151
- fileExtension = if (asset.type!!.startsWith(".")) asset.type else "." + asset.type
152
- }
151
+ val fileExtension = asset.getFileExtension()
153
152
  return if (asset.key == null) {
154
153
  // create a filename that's unlikely to collide with any other asset
155
154
  "asset-" + Date().time + "-" + Random().nextInt() + fileExtension
@@ -5,6 +5,7 @@ import androidx.room.*
5
5
  import expo.modules.updates.db.entity.AssetEntity
6
6
  import expo.modules.updates.db.entity.UpdateAssetEntity
7
7
  import expo.modules.updates.db.entity.UpdateEntity
8
+ import expo.modules.updates.utils.AndroidResourceAssetUtils
8
9
  import java.util.*
9
10
 
10
11
  /**
@@ -88,12 +89,17 @@ abstract class AssetDao {
88
89
  }
89
90
 
90
91
  fun loadAssetWithKey(key: String?): AssetEntity? {
91
- val assets = loadAssetWithKeyInternal(key)
92
- return if (assets.isNotEmpty()) {
93
- assets[0]
94
- } else {
95
- null
92
+ val asset = loadAssetWithKeyInternal(key).firstOrNull() ?: return null
93
+
94
+ // Load some properties not stored in database but can be computed from other fields
95
+ asset.relativePath?.let {
96
+ val (embeddedAssetFilename, resourceFolder, resourceFilename) =
97
+ AndroidResourceAssetUtils.parseAndroidResponseAssetFromPath(it)
98
+ asset.embeddedAssetFilename = embeddedAssetFilename
99
+ asset.resourcesFolder = resourceFolder
100
+ asset.resourcesFilename = resourceFilename
96
101
  }
102
+ return asset
97
103
  }
98
104
 
99
105
  fun mergeAndUpdateAsset(existingEntity: AssetEntity, newEntity: AssetEntity) {
@@ -119,11 +125,11 @@ abstract class AssetDao {
119
125
  // we need to keep track of whether the calling class expects this asset to be the launch asset
120
126
  existingEntity.isLaunchAsset = newEntity.isLaunchAsset
121
127
  // some fields on the asset entity are not stored in the database but might still be used by application code
122
- existingEntity.embeddedAssetFilename = newEntity.embeddedAssetFilename
123
- existingEntity.resourcesFilename = newEntity.resourcesFilename
124
- existingEntity.resourcesFolder = newEntity.resourcesFolder
125
- existingEntity.scale = newEntity.scale
126
- existingEntity.scales = newEntity.scales
128
+ newEntity.embeddedAssetFilename?.let { existingEntity.embeddedAssetFilename = it }
129
+ newEntity.resourcesFilename?.let { existingEntity.resourcesFilename = it }
130
+ newEntity.resourcesFolder?.let { existingEntity.resourcesFolder = it }
131
+ newEntity.scale?.let { existingEntity.scale = it }
132
+ newEntity.scales?.let { existingEntity.scales = it }
127
133
  }
128
134
 
129
135
  @Transaction
@@ -68,4 +68,13 @@ class AssetEntity(@field:ColumnInfo(name = "key") var key: String?, var type: St
68
68
 
69
69
  @Ignore
70
70
  var scales: Array<Float>? = null
71
+
72
+ internal fun getFileExtension(): String {
73
+ val type = this.type ?: return ""
74
+ return if (type.startsWith(".")) {
75
+ type
76
+ } else {
77
+ ".$type"
78
+ }
79
+ }
71
80
  }
@@ -2,6 +2,8 @@ package expo.modules.updates.launcher
2
2
 
3
3
  import android.content.Context
4
4
  import android.net.Uri
5
+ import androidx.annotation.VisibleForTesting
6
+ import expo.modules.updates.BuildConfig
5
7
  import expo.modules.updates.UpdatesConfiguration
6
8
  import expo.modules.updates.UpdatesUtils
7
9
  import expo.modules.updates.db.UpdatesDatabase
@@ -18,6 +20,7 @@ import expo.modules.updates.manifest.EmbeddedManifestUtils
18
20
  import expo.modules.updates.manifest.EmbeddedUpdate
19
21
  import expo.modules.updates.manifest.ManifestMetadata
20
22
  import expo.modules.updates.selectionpolicy.SelectionPolicy
23
+ import expo.modules.updates.utils.AndroidResourceAssetUtils
21
24
  import org.json.JSONObject
22
25
  import java.io.File
23
26
 
@@ -44,7 +47,8 @@ class DatabaseLauncher(
44
47
  private val updatesDirectory: File?,
45
48
  private val fileDownloader: FileDownloader,
46
49
  private val selectionPolicy: SelectionPolicy,
47
- private val logger: UpdatesLogger
50
+ private val logger: UpdatesLogger,
51
+ private val shouldCopyEmbeddedAssets: Boolean = BuildConfig.EX_UPDATES_COPY_EMBEDDED_ASSETS
48
52
  ) : Launcher {
49
53
  private val loaderFiles: LoaderFiles = LoaderFiles()
50
54
  override var launchedUpdate: UpdateEntity? = null
@@ -97,7 +101,18 @@ class DatabaseLauncher(
97
101
  val embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, configuration)
98
102
  val extraHeaders = FileDownloader.getExtraHeadersForRemoteAssetRequest(launchedUpdate, embeddedUpdate?.updateEntity, launchedUpdate)
99
103
 
100
- val launchAssetFile = ensureAssetExists(launchAsset, database, embeddedUpdate, extraHeaders)
104
+ val embeddedLaunchAsset = if (!shouldCopyEmbeddedAssets) {
105
+ embeddedUpdate?.assetEntityList
106
+ ?.find { it.key == launchAsset.key }
107
+ ?.embeddedAssetFilename
108
+ ?.let {
109
+ // react-native uses `assets://` to indicate loading a bundle from assets
110
+ "assets://$it"
111
+ }
112
+ } else {
113
+ null
114
+ }
115
+ val launchAssetFile = embeddedLaunchAsset ?: ensureAssetExists(launchAsset, database, embeddedUpdate, extraHeaders)
101
116
  if (launchAssetFile != null) {
102
117
  this.launchAssetFile = launchAssetFile.toString()
103
118
  }
@@ -110,12 +125,14 @@ class DatabaseLauncher(
110
125
  // we took care of this one above
111
126
  continue
112
127
  }
113
- val filename = asset.relativePath
114
- if (filename != null) {
128
+ val filename = asset.relativePath ?: continue
129
+ if (!AndroidResourceAssetUtils.isAndroidResourceAsset(filename)) {
115
130
  val assetFile = ensureAssetExists(asset, database, embeddedUpdate, extraHeaders)
116
131
  if (assetFile != null) {
117
132
  this[asset] = Uri.fromFile(assetFile).toString()
118
133
  }
134
+ } else {
135
+ this[asset] = filename
119
136
  }
120
137
  }
121
138
  }
@@ -158,6 +175,20 @@ class DatabaseLauncher(
158
175
  if (asset.isLaunchAsset) {
159
176
  continue
160
177
  }
178
+
179
+ if (!shouldCopyEmbeddedAssets) {
180
+ val filename = AndroidResourceAssetUtils.createEmbeddedFilenameForAsset(asset)
181
+ if (filename != null) {
182
+ asset.relativePath = filename
183
+ this[asset] = filename
184
+ logger.info("embeddedAssetFileMap: ${asset.key},${asset.type} => ${this[asset]}")
185
+ } else {
186
+ val cause = Exception("Missing embedded asset")
187
+ logger.error("embeddedAssetFileMap: no file for ${asset.key},${asset.type}", cause, UpdatesErrorCode.AssetsFailedToLoad)
188
+ }
189
+ continue
190
+ }
191
+
161
192
  val filename = UpdatesUtils.createFilenameForAsset(asset)
162
193
  asset.relativePath = filename
163
194
  val assetFile = File(updatesDirectory, filename)
@@ -175,7 +206,13 @@ class DatabaseLauncher(
175
206
  }
176
207
  }
177
208
 
178
- fun ensureAssetExists(asset: AssetEntity, database: UpdatesDatabase, embeddedUpdate: EmbeddedUpdate?, extraHeaders: JSONObject): File? {
209
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
210
+ fun ensureAssetExists(
211
+ asset: AssetEntity,
212
+ database: UpdatesDatabase,
213
+ embeddedUpdate: EmbeddedUpdate?,
214
+ extraHeaders: JSONObject
215
+ ): File? {
179
216
  val assetFile = File(updatesDirectory, asset.relativePath ?: "")
180
217
  var assetFileExists = assetFile.exists()
181
218
  if (!assetFileExists) {
@@ -1,6 +1,7 @@
1
1
  package expo.modules.updates.loader
2
2
 
3
3
  import android.content.Context
4
+ import expo.modules.updates.BuildConfig
4
5
  import expo.modules.updates.UpdatesConfiguration
5
6
  import expo.modules.updates.db.entity.AssetEntity
6
7
  import expo.modules.updates.db.UpdatesDatabase
@@ -9,6 +10,7 @@ import expo.modules.updates.loader.FileDownloader.RemoteUpdateDownloadCallback
9
10
  import expo.modules.updates.UpdatesUtils
10
11
  import expo.modules.updates.db.entity.UpdateEntity
11
12
  import expo.modules.updates.logging.UpdatesLogger
13
+ import expo.modules.updates.utils.AndroidResourceAssetUtils
12
14
  import java.io.File
13
15
  import java.io.FileNotFoundException
14
16
  import java.lang.AssertionError
@@ -16,14 +18,14 @@ import java.lang.Exception
16
18
  import java.util.*
17
19
 
18
20
  /**
19
- * Subclass of [Loader] which handles copying the embedded update's assets into the
20
- * expo-updates cache location.
21
+ * Subclass of [Loader] which handles embedded update assets
21
22
  *
22
- * Rather than launching the embedded update directly from its location in the app bundle/apk, we
23
- * first try to read it into the expo-updates cache and database and launch it like any other
24
- * update. The benefits of this include (a) a single code path for launching most updates and (b)
25
- * assets included in embedded updates and copied into the cache in this way do not need to be
26
- * re-downloaded if included in future updates.
23
+ * @param shouldCopyEmbeddedAssets if true, copying the embedded update's assets into the expo-updates cache location.
24
+ * Rather than launching the embedded update directly from its location in the app bundle/apk, we
25
+ * first try to read it into the expo-updates cache and database and launch it like any other
26
+ * update. The benefits of this include (a) a single code path for launching most updates and (b)
27
+ * assets included in embedded updates and copied into the cache in this way do not need to be
28
+ * re-downloaded if included in future updates.
27
29
  */
28
30
  class EmbeddedLoader internal constructor(
29
31
  context: Context,
@@ -31,7 +33,8 @@ class EmbeddedLoader internal constructor(
31
33
  logger: UpdatesLogger,
32
34
  database: UpdatesDatabase,
33
35
  updatesDirectory: File,
34
- private val loaderFiles: LoaderFiles
36
+ private val loaderFiles: LoaderFiles,
37
+ private val shouldCopyEmbeddedAssets: Boolean = BuildConfig.EX_UPDATES_COPY_EMBEDDED_ASSETS
35
38
  ) : Loader(
36
39
  context,
37
40
  configuration,
@@ -76,10 +79,19 @@ class EmbeddedLoader internal constructor(
76
79
  embeddedUpdate: UpdateEntity?,
77
80
  callback: AssetDownloadCallback
78
81
  ) {
82
+ if (!shouldCopyEmbeddedAssets) {
83
+ assetEntity.downloadTime = Date()
84
+ assetEntity.relativePath = AndroidResourceAssetUtils.createEmbeddedFilenameForAsset(assetEntity)
85
+ // Passing `isNew=true` aka `AssetLoadResult.FINISHED` to the callback,
86
+ // because we assume embedded asset is always existed without filesystem out of sync.
87
+ callback.onSuccess(assetEntity, true)
88
+ return
89
+ }
90
+
79
91
  val filename = UpdatesUtils.createFilenameForAsset(assetEntity)
80
92
  val destination = File(updatesDirectory, filename)
81
93
 
82
- if (loaderFiles.fileExists(destination)) {
94
+ if (loaderFiles.fileExists(context, updatesDirectory, filename)) {
83
95
  assetEntity.relativePath = filename
84
96
  callback.onSuccess(assetEntity, false)
85
97
  } else {
@@ -233,12 +233,8 @@ abstract class Loader protected constructor(
233
233
  }
234
234
 
235
235
  // if we already have a local copy of this asset, don't try to download it again!
236
- if (assetEntity.relativePath != null && loaderFiles.fileExists(
237
- File(
238
- updatesDirectory,
239
- assetEntity.relativePath
240
- )
241
- )
236
+ if (assetEntity.relativePath != null &&
237
+ loaderFiles.fileExists(context, updatesDirectory, assetEntity.relativePath)
242
238
  ) {
243
239
  handleAssetDownloadCompleted(assetEntity, AssetLoadResult.ALREADY_EXISTS)
244
240
  continue
@@ -8,6 +8,7 @@ import expo.modules.updates.UpdatesUtils
8
8
  import expo.modules.updates.db.entity.AssetEntity
9
9
  import expo.modules.updates.manifest.EmbeddedManifestUtils
10
10
  import expo.modules.updates.manifest.Update
11
+ import expo.modules.updates.utils.AndroidResourceAssetUtils
11
12
  import java.io.File
12
13
  import java.io.IOException
13
14
  import java.security.NoSuchAlgorithmException
@@ -16,8 +17,12 @@ import java.security.NoSuchAlgorithmException
16
17
  * Utility class for Loader and its subclasses, to allow for easy mocking
17
18
  */
18
19
  open class LoaderFiles {
19
- fun fileExists(destination: File): Boolean {
20
- return destination.exists()
20
+ fun fileExists(context: Context, updateDirectory: File?, relativePath: String?): Boolean {
21
+ val filePath = relativePath ?: return false
22
+ if (AndroidResourceAssetUtils.isAndroidAssetOrResourceExisted(context, filePath)) {
23
+ return true
24
+ }
25
+ return File(updateDirectory, filePath).exists()
21
26
  }
22
27
 
23
28
  fun readEmbeddedUpdate(
@@ -0,0 +1,120 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ package expo.modules.updates.utils
4
+
5
+ import android.annotation.SuppressLint
6
+ import android.content.Context
7
+ import androidx.core.net.toUri
8
+ import expo.modules.core.errors.InvalidArgumentException
9
+ import expo.modules.updates.db.entity.AssetEntity
10
+ import java.io.IOException
11
+
12
+ /**
13
+ * Helpers for Android embedded assets and resources
14
+ */
15
+ internal object AndroidResourceAssetUtils {
16
+ private const val ANDROID_EMBEDDED_URL_BASE_ASSET = "file:///android_asset/"
17
+ private const val ANDROID_EMBEDDED_URL_BASE_RESOURCE = "file:///android_res/"
18
+
19
+ /**
20
+ * Create an embedded asset filename in `file:///android_res/` or `file:///android_asset/` format
21
+ */
22
+ fun createEmbeddedFilenameForAsset(asset: AssetEntity): String? {
23
+ val fileExtension = asset.getFileExtension()
24
+ if (asset.embeddedAssetFilename != null) {
25
+ return "${ANDROID_EMBEDDED_URL_BASE_ASSET}${asset.embeddedAssetFilename}$fileExtension"
26
+ }
27
+ if (asset.resourcesFolder != null && asset.resourcesFilename != null) {
28
+ return "${ANDROID_EMBEDDED_URL_BASE_RESOURCE}${asset.resourcesFolder}${getDrawableSuffix(asset.scale)}/${asset.resourcesFilename}$fileExtension"
29
+ }
30
+ return null
31
+ }
32
+
33
+ /**
34
+ * Return whether the filePath is an Android asset or resource
35
+ */
36
+ fun isAndroidResourceAsset(filePath: String): Boolean {
37
+ return filePath.startsWith(ANDROID_EMBEDDED_URL_BASE_RESOURCE) ||
38
+ filePath.startsWith(ANDROID_EMBEDDED_URL_BASE_ASSET)
39
+ }
40
+
41
+ /**
42
+ * Check a given file name is existed in Android embedded assets
43
+ */
44
+ fun isAndroidAssetExisted(context: Context, name: String) = try {
45
+ context.assets.open(name).close()
46
+ true
47
+ } catch (e: IOException) {
48
+ false
49
+ }
50
+
51
+ /**
52
+ * Check a given resource folder and filename is existed in Android embedded resources
53
+ */
54
+ @SuppressLint("DiscouragedApi")
55
+ fun isAndroidResourceExisted(context: Context, resourceFolder: String, resourceFilename: String): Boolean {
56
+ return context.resources.getIdentifier(
57
+ resourceFilename,
58
+ resourceFolder,
59
+ context.packageName
60
+ ) != 0
61
+ }
62
+
63
+ /**
64
+ * Check if given filePath matches and exists in the Android embedded assets or resources
65
+ */
66
+ fun isAndroidAssetOrResourceExisted(context: Context, filePath: String): Boolean {
67
+ val (embeddedAssetFilename, resourceFolder, resourceFilename) = parseAndroidResponseAssetFromPath(filePath)
68
+ return when {
69
+ embeddedAssetFilename != null -> isAndroidAssetExisted(context, embeddedAssetFilename)
70
+ resourceFolder != null && resourceFilename != null -> isAndroidResourceExisted(context, resourceFolder, resourceFilename)
71
+ else -> {
72
+ false
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Data structure for Android embedded asset and resource
79
+ */
80
+ data class AndroidResourceAsset(
81
+ val embeddedAssetFilename: String?,
82
+ val resourcesFolder: String?,
83
+ val resourceFilename: String?
84
+ )
85
+
86
+ /**
87
+ * Parse a file path and return as `AndroidResourceAsset`
88
+ */
89
+ fun parseAndroidResponseAssetFromPath(filePath: String): AndroidResourceAsset {
90
+ if (filePath.startsWith(ANDROID_EMBEDDED_URL_BASE_RESOURCE)) {
91
+ val uri = filePath.toUri()
92
+ val pathSegments = uri.pathSegments
93
+ if (pathSegments.size < 3) {
94
+ throw InvalidArgumentException("Invalid resource file path: $filePath")
95
+ }
96
+ // Strip any qualifiers after a dash, for example "drawable-xhdpi" becomes "drawable"
97
+ val resourcesFolder = pathSegments[1].substringBefore('-')
98
+ // Strip file extension for resource name
99
+ val resourceFilename = pathSegments[2].substringBeforeLast('.', pathSegments[2])
100
+ return AndroidResourceAsset(null, resourcesFolder, resourceFilename)
101
+ }
102
+ if (filePath.startsWith(ANDROID_EMBEDDED_URL_BASE_ASSET)) {
103
+ val embeddedAssetFilename = filePath.substringAfterLast('/')
104
+ return AndroidResourceAsset(embeddedAssetFilename, null, null)
105
+ }
106
+ return AndroidResourceAsset(null, null, null)
107
+ }
108
+
109
+ private fun getDrawableSuffix(scale: Float?): String {
110
+ return when (scale) {
111
+ 0.75f -> "-ldpi"
112
+ 1f -> "-mdpi"
113
+ 1.5f -> "-hdpi"
114
+ 2f -> "-xhdpi"
115
+ 3f -> "-xxhdpi"
116
+ 4f -> "-xxxhdpi"
117
+ else -> ""
118
+ }
119
+ }
120
+ }