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.
- package/CHANGELOG.md +10 -0
- package/android/build.gradle +11 -6
- package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +4 -5
- package/android/src/main/java/expo/modules/updates/db/dao/AssetDao.kt +16 -10
- package/android/src/main/java/expo/modules/updates/db/entity/AssetEntity.kt +9 -0
- package/android/src/main/java/expo/modules/updates/launcher/DatabaseLauncher.kt +42 -5
- package/android/src/main/java/expo/modules/updates/loader/EmbeddedLoader.kt +21 -9
- package/android/src/main/java/expo/modules/updates/loader/Loader.kt +2 -6
- package/android/src/main/java/expo/modules/updates/loader/LoaderFiles.kt +7 -2
- package/android/src/main/java/expo/modules/updates/utils/AndroidResourceAssetUtils.kt +120 -0
- package/bin/check-for-changed-paths/index.js +4 -0
- package/build/ExpoUpdates.web.d.ts.map +1 -1
- package/build/ExpoUpdates.web.js +1 -1
- package/build/ExpoUpdates.web.js.map +1 -1
- package/e2e/fixtures/Updates.e2e.ts +143 -113
- package/e2e/fixtures/project_files/scripts/check-android-emulator.ts +44 -0
- package/e2e/setup/check-for-changed-paths.ts +128 -0
- package/e2e/setup/create-eas-project-tv.ts +5 -2
- package/e2e/setup/create-eas-project.ts +5 -2
- package/e2e/setup/paths-filter/LICENSE +22 -0
- package/e2e/setup/paths-filter/README.md +567 -0
- package/e2e/setup/paths-filter/file.ts +13 -0
- package/e2e/setup/paths-filter/filter.ts +164 -0
- package/e2e/setup/paths-filter/git.ts +313 -0
- package/e2e/setup/paths-filter/paths-filter-dependencies.ts +32 -0
- package/e2e/setup/project.ts +1 -0
- package/e2e/tsconfig.json +4 -0
- package/package.json +13 -7
- 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._
|
package/android/build.gradle
CHANGED
|
@@ -39,7 +39,7 @@ expoModule {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
group = 'host.exp.exponent'
|
|
42
|
-
version = '0.28.
|
|
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.
|
|
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.
|
|
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.
|
|
147
|
-
androidTestImplementation 'androidx.test:core:1.
|
|
148
|
-
androidTestImplementation 'androidx.test:rules:1.
|
|
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
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 =
|
|
123
|
-
existingEntity.resourcesFilename =
|
|
124
|
-
existingEntity.resourcesFolder =
|
|
125
|
-
existingEntity.scale =
|
|
126
|
-
existingEntity.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
|
|
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
|
|
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
|
-
|
|
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
|
|
20
|
-
* expo-updates cache location.
|
|
21
|
+
* Subclass of [Loader] which handles embedded update assets
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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(
|
|
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 &&
|
|
237
|
-
|
|
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(
|
|
20
|
-
return
|
|
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
|
+
}
|