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