1 package expo.modules.updates
2 
3 import android.content.Context
4 import expo.modules.updates.db.entity.AssetEntity
5 import expo.modules.updates.db.entity.UpdateEntity
6 import expo.modules.updates.launcher.DatabaseLauncher
7 import expo.modules.updates.launcher.Launcher.LauncherCallback
8 import expo.modules.updates.loader.Loader
9 import expo.modules.updates.loader.RemoteLoader
10 import expo.modules.updates.loader.UpdateDirective
11 import expo.modules.updates.loader.UpdateResponse
12 import expo.modules.updates.selectionpolicy.LauncherSelectionPolicySingleUpdate
13 import expo.modules.updates.selectionpolicy.ReaperSelectionPolicyDevelopmentClient
14 import expo.modules.updates.selectionpolicy.SelectionPolicy
15 import expo.modules.updatesinterface.UpdatesInterface
16 import expo.modules.updatesinterface.UpdatesInterface.QueryCallback
17 import expo.modules.updatesinterface.UpdatesInterface.UpdateCallback
18 import org.json.JSONObject
19 import java.util.*
20 
21 /**
22  * Main entry point to expo-updates in development builds with expo-dev-client. Singleton that still
23  * makes use of [UpdatesController] for keeping track of updates state, but provides capabilities
24  * that are not usually exposed but that expo-dev-client needs (launching and downloading a specific
25  * update by URL, allowing dynamic configuration, introspecting the database).
26  *
27  * Implements the external UpdatesInterface from the expo-updates-interface package. This allows
28  * expo-dev-client to compile without needing expo-updates to be installed.
29  */
30 class UpdatesDevLauncherController : UpdatesInterface {
31   private var mTempConfiguration: UpdatesConfiguration? = null
resetnull32   override fun reset() {
33     UpdatesController.instance.setLauncher(null)
34   }
35 
36   /**
37    * Fetch an update using a dynamically generated configuration object (including a potentially
38    * different update URL than the one embedded in the build).
39    */
fetchUpdateWithConfigurationnull40   override fun fetchUpdateWithConfiguration(
41     configuration: HashMap<String, Any>,
42     context: Context,
43     callback: UpdateCallback
44   ) {
45     val controller = UpdatesController.instance
46     val updatesConfiguration = UpdatesConfiguration(context, configuration)
47     if (updatesConfiguration.updateUrl == null || updatesConfiguration.scopeKey == null) {
48       callback.onFailure(Exception("Failed to load update: UpdatesConfiguration object must include a valid update URL"))
49       return
50     }
51     if (controller.updatesDirectory == null) {
52       callback.onFailure(controller.updatesDirectoryException)
53       return
54     }
55 
56     // since controller is a singleton, save its config so we can reset to it if our request fails
57     mTempConfiguration = controller.updatesConfiguration
58     setDevelopmentSelectionPolicy()
59     controller.updatesConfiguration = updatesConfiguration
60     val databaseHolder = controller.databaseHolder
61     val loader = RemoteLoader(
62       context,
63       updatesConfiguration,
64       databaseHolder.database,
65       controller.fileDownloader,
66       controller.updatesDirectory,
67       null
68     )
69     loader.start(object : Loader.LoaderCallback {
70       override fun onFailure(e: Exception) {
71         databaseHolder.releaseDatabase()
72         // reset controller's configuration to what it was before this request
73         controller.updatesConfiguration = mTempConfiguration!!
74         callback.onFailure(e)
75       }
76 
77       override fun onSuccess(loaderResult: Loader.LoaderResult) {
78         // the dev launcher doesn't handle roll back to embedded commands
79         databaseHolder.releaseDatabase()
80         if (loaderResult.updateEntity == null) {
81           callback.onSuccess(null)
82           return
83         }
84         launchUpdate(loaderResult.updateEntity, updatesConfiguration, context, callback)
85       }
86 
87       override fun onAssetLoaded(
88         asset: AssetEntity,
89         successfulAssetCount: Int,
90         failedAssetCount: Int,
91         totalAssetCount: Int
92       ) {
93         callback.onProgress(successfulAssetCount, failedAssetCount, totalAssetCount)
94       }
95 
96       override fun onUpdateResponseLoaded(updateResponse: UpdateResponse): Loader.OnUpdateResponseLoadedResult {
97         val updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective
98         if (updateDirective != null) {
99           return Loader.OnUpdateResponseLoadedResult(
100             shouldDownloadManifestIfPresentInResponse = when (updateDirective) {
101               is UpdateDirective.RollBackToEmbeddedUpdateDirective -> false
102               is UpdateDirective.NoUpdateAvailableUpdateDirective -> false
103             }
104           )
105         }
106 
107         val updateManifest = updateResponse.manifestUpdateResponsePart?.updateManifest ?: return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = false)
108         return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = callback.onManifestLoaded(updateManifest.manifest.getRawJson()))
109       }
110     })
111   }
112 
launchUpdatenull113   private fun launchUpdate(
114     update: UpdateEntity,
115     configuration: UpdatesConfiguration,
116     context: Context,
117     callback: UpdateCallback
118   ) {
119     val controller = UpdatesController.instance
120 
121     // ensure that we launch the update we want, even if it isn't the latest one
122     val currentSelectionPolicy = controller.selectionPolicy
123     // Calling `setNextSelectionPolicy` allows the Updates module's `reloadAsync` method to reload
124     // with a different (newer) update if one is downloaded, e.g. using `fetchUpdateAsync`. If we
125     // set the default selection policy here instead, the update we are launching here would keep
126     // being launched by `reloadAsync` even if a newer one is downloaded.
127     controller.setNextSelectionPolicy(
128       SelectionPolicy(
129         LauncherSelectionPolicySingleUpdate(update.id),
130         currentSelectionPolicy.loaderSelectionPolicy,
131         currentSelectionPolicy.reaperSelectionPolicy
132       )
133     )
134 
135     val databaseHolder = controller.databaseHolder
136     val launcher = DatabaseLauncher(
137       configuration,
138       controller.updatesDirectory!!,
139       controller.fileDownloader,
140       controller.selectionPolicy
141     )
142     launcher.launch(
143       databaseHolder.database, context,
144       object : LauncherCallback {
145         override fun onFailure(e: Exception) {
146           databaseHolder.releaseDatabase()
147           // reset controller's configuration to what it was before this request
148           controller.updatesConfiguration = mTempConfiguration!!
149           callback.onFailure(e)
150         }
151 
152         override fun onSuccess() {
153           databaseHolder.releaseDatabase()
154           controller.setLauncher(launcher)
155           callback.onSuccess(object : UpdatesInterface.Update {
156             override fun getManifest(): JSONObject {
157               return launcher.launchedUpdate!!.manifest
158             }
159 
160             override fun getLaunchAssetPath(): String {
161               return launcher.launchAssetFile!!
162             }
163           })
164           controller.runReaper()
165         }
166       }
167     )
168   }
169 
storedUpdateIdsWithConfigurationnull170   override fun storedUpdateIdsWithConfiguration(configuration: HashMap<String, Any>, context: Context, callback: QueryCallback) {
171     val controller = UpdatesController.instance
172     val updatesConfiguration = UpdatesConfiguration(context, configuration)
173     if (updatesConfiguration.updateUrl == null || updatesConfiguration.scopeKey == null) {
174       callback.onFailure(Exception("Failed to load update: UpdatesConfiguration object must include a valid update URL"))
175       return
176     }
177     val updatesDirectory = controller.updatesDirectory
178     if (updatesDirectory == null) {
179       callback.onFailure(controller.updatesDirectoryException)
180       return
181     }
182     val databaseHolder = controller.databaseHolder
183     val launcher = DatabaseLauncher(
184       updatesConfiguration,
185       updatesDirectory,
186       controller.fileDownloader,
187       controller.selectionPolicy
188     )
189     val readyUpdateIds = launcher.getReadyUpdateIds(databaseHolder.database)
190     controller.databaseHolder.releaseDatabase()
191     callback.onSuccess(readyUpdateIds)
192   }
193 
194   companion object {
195     private var singletonInstance: UpdatesDevLauncherController? = null
196     val instance: UpdatesDevLauncherController
197       get() {
<lambda>null198         return checkNotNull(singletonInstance) { "UpdatesDevLauncherController.instance was called before the module was initialized" }
199       }
200 
initializenull201     @JvmStatic fun initialize(context: Context): UpdatesDevLauncherController {
202       if (singletonInstance == null) {
203         singletonInstance = UpdatesDevLauncherController()
204       }
205       UpdatesController.initializeWithoutStarting(context)
206       return instance
207     }
208 
setDevelopmentSelectionPolicynull209     private fun setDevelopmentSelectionPolicy() {
210       val controller = UpdatesController.instance
211       controller.resetSelectionPolicyToDefault()
212       val currentSelectionPolicy = controller.selectionPolicy
213       controller.setDefaultSelectionPolicy(
214         SelectionPolicy(
215           currentSelectionPolicy.launcherSelectionPolicy,
216           currentSelectionPolicy.loaderSelectionPolicy,
217           ReaperSelectionPolicyDevelopmentClient()
218         )
219       )
220       controller.resetSelectionPolicyToDefault()
221     }
222   }
223 }
224