1 package expo.modules.updates.loader
2 
3 import android.content.Context
4 import expo.modules.updates.UpdatesConfiguration
5 import expo.modules.updates.db.entity.AssetEntity
6 import expo.modules.updates.db.UpdatesDatabase
7 import expo.modules.updates.loader.FileDownloader.AssetDownloadCallback
8 import expo.modules.updates.loader.FileDownloader.RemoteUpdateDownloadCallback
9 import expo.modules.updates.UpdatesUtils
10 import java.io.File
11 import java.io.FileNotFoundException
12 import java.lang.AssertionError
13 import java.lang.Exception
14 import java.util.*
15 
16 /**
17  * Subclass of [Loader] which handles copying the embedded update's assets into the
18  * expo-updates cache location.
19  *
20  * Rather than launching the embedded update directly from its location in the app bundle/apk, we
21  * first try to read it into the expo-updates cache and database and launch it like any other
22  * update. The benefits of this include (a) a single code path for launching most updates and (b)
23  * assets included in embedded updates and copied into the cache in this way do not need to be
24  * redownloaded if included in future updates.
25  *
26  * However, if a visual asset is included at multiple scales in an embedded update, we don't have
27  * access to and must skip copying scales that don't match the resolution of the current device. In
28  * this case, we cannot fully copy the embedded update, and instead launch it from the original
29  * location. We still copy the assets we can so they don't need to be redownloaded in the future.
30  */
31 class EmbeddedLoader internal constructor(
32   private val context: Context,
33   private val configuration: UpdatesConfiguration,
34   database: UpdatesDatabase,
35   updatesDirectory: File?,
36   private val loaderFiles: LoaderFiles
37 ) : Loader(
38   context, configuration, database, updatesDirectory, loaderFiles
39 ) {
40   private val pixelDensity = context.resources.displayMetrics.density
41 
42   constructor(
43     context: Context,
44     configuration: UpdatesConfiguration,
45     database: UpdatesDatabase,
46     updatesDirectory: File?
47   ) : this(context, configuration, database, updatesDirectory, LoaderFiles()) {
48   }
49 
loadRemoteUpdatenull50   override fun loadRemoteUpdate(
51     context: Context,
52     database: UpdatesDatabase,
53     configuration: UpdatesConfiguration,
54     callback: RemoteUpdateDownloadCallback
55   ) {
56     val updateManifest = loaderFiles.readEmbeddedManifest(this.context, this.configuration)
57     if (updateManifest != null) {
58       callback.onSuccess(
59         UpdateResponse(
60           responseHeaderData = null,
61           manifestUpdateResponsePart = UpdateResponsePart.ManifestUpdateResponsePart(updateManifest),
62           directiveUpdateResponsePart = null
63         )
64       )
65     } else {
66       val message = "Embedded manifest is null"
67       callback.onFailure(message, Exception(message))
68     }
69   }
70 
loadAssetnull71   override fun loadAsset(
72     context: Context,
73     assetEntity: AssetEntity,
74     updatesDirectory: File?,
75     configuration: UpdatesConfiguration,
76     callback: AssetDownloadCallback
77   ) {
78     val filename = UpdatesUtils.createFilenameForAsset(assetEntity)
79     val destination = File(updatesDirectory, filename)
80 
81     if (loaderFiles.fileExists(destination)) {
82       assetEntity.relativePath = filename
83       callback.onSuccess(assetEntity, false)
84     } else {
85       try {
86         assetEntity.hash = loaderFiles.copyAssetAndGetHash(assetEntity, destination, context)
87         assetEntity.downloadTime = Date()
88         assetEntity.relativePath = filename
89         callback.onSuccess(assetEntity, true)
90       } catch (e: FileNotFoundException) {
91         throw AssertionError(
92           "APK bundle must contain the expected embedded asset " +
93             if (assetEntity.embeddedAssetFilename != null) assetEntity.embeddedAssetFilename else assetEntity.resourcesFilename
94         )
95       } catch (e: Exception) {
96         callback.onFailure(e, assetEntity)
97       }
98     }
99   }
100 
shouldSkipAssetnull101   override fun shouldSkipAsset(assetEntity: AssetEntity): Boolean {
102     return if (assetEntity.scales == null || assetEntity.scale == null) {
103       false
104     } else pickClosestScale(assetEntity.scales!!) != assetEntity.scale
105   }
106 
107   // https://developer.android.com/guide/topics/resources/providing-resources.html#BestMatch
108   // If a perfect match is not available, the OS will pick the next largest scale.
109   // If only smaller scales are available, the OS will choose the largest available one.
pickClosestScalenull110   private fun pickClosestScale(scales: Array<Float>): Float {
111     var closestScale = Float.MAX_VALUE
112     var largestScale = 0f
113     for (scale in scales) {
114       if (scale >= pixelDensity && scale < closestScale) {
115         closestScale = scale
116       }
117       if (scale > largestScale) {
118         largestScale = scale
119       }
120     }
121     return if (closestScale < Float.MAX_VALUE) closestScale else largestScale
122   }
123 
124   companion object {
125     private val TAG = EmbeddedLoader::class.java.simpleName
126 
127     const val BUNDLE_FILENAME = "app.bundle"
128     const val BARE_BUNDLE_FILENAME = "index.android.bundle"
129   }
130 }
131