1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent
3 
4 import android.content.Context
5 import android.graphics.Bitmap
6 import android.graphics.BitmapFactory
7 import android.graphics.Color
8 import android.net.Uri
9 import android.os.AsyncTask
10 import android.text.TextUtils
11 import android.util.LruCache
12 import expo.modules.manifests.core.InternalJSONMutator
13 import expo.modules.manifests.core.Manifest
14 import host.exp.exponent.analytics.EXL
15 import host.exp.exponent.generated.ExponentBuildConstants
16 import host.exp.exponent.kernel.ExponentUrls
17 import host.exp.exponent.kernel.KernelProvider
18 import host.exp.exponent.storage.ExponentSharedPreferences
19 import host.exp.exponent.utils.ColorParser
20 import host.exp.expoview.R
21 import org.apache.commons.io.IOUtils
22 import org.json.JSONException
23 import org.json.JSONObject
24 import java.io.IOException
25 import java.net.HttpURLConnection
26 import java.net.URL
27 import javax.inject.Inject
28 import javax.inject.Singleton
29 import kotlin.math.max
30 
31 data class ManifestAndAssetRequestHeaders(val manifest: Manifest, val assetRequestHeaders: JSONObject)
32 
33 @Singleton
34 class ExponentManifest @Inject constructor(
35   var context: Context,
36   var exponentSharedPreferences: ExponentSharedPreferences
37 ) {
38   interface BitmapListener {
onLoadBitmapnull39     fun onLoadBitmap(bitmap: Bitmap?)
40   }
41 
42   private val memoryCache: LruCache<String, Bitmap>
43 
44   fun httpManifestUrl(manifestUrl: String): Uri {
45     return httpManifestUrlBuilder(manifestUrl).build()
46   }
47 
httpManifestUrlBuildernull48   private fun httpManifestUrlBuilder(manifestUrl: String): Uri.Builder {
49     var realManifestUrl = manifestUrl
50     if (manifestUrl.contains(REDIRECT_SNIPPET)) {
51       // Redirect urls look like "https://exp.host/--/to-exp/exp%3A%2F%2Fgj-5x6.jesse.internal.exp.direct%3A80".
52       // Android is crazy and catches this url with this intent filter:
53       //  <data
54       //    android:host="*.exp.direct"
55       //    android:pathPattern=".*"
56       //    android:scheme="http"/>
57       //  <data
58       //    android:host="*.exp.direct"
59       //    android:pathPattern=".*"
60       //    android:scheme="https"/>
61       // so we have to add some special logic to handle that. This is than handling arbitrary HTTP 301s and 302
62       realManifestUrl = Uri.decode(
63         realManifestUrl.substring(
64           realManifestUrl.indexOf(
65             REDIRECT_SNIPPET
66           ) + REDIRECT_SNIPPET.length
67         )
68       )
69     }
70     val httpManifestUrl = ExponentUrls.toHttp(realManifestUrl)
71     val uri = Uri.parse(httpManifestUrl)
72     var newPath = uri.path
73     if (newPath == null) {
74       newPath = ""
75     }
76     val deepLinkIndex = newPath.indexOf(DEEP_LINK_SEPARATOR_WITH_SLASH)
77     if (deepLinkIndex > -1) {
78       newPath = newPath.substring(0, deepLinkIndex)
79     }
80     return uri.buildUpon().encodedPath(newPath)
81   }
82 
loadIconBitmapnull83   fun loadIconBitmap(iconUrl: String?, listener: BitmapListener) {
84     val icon = getIconFromCache(iconUrl)
85     if (icon != null) {
86       listener.onLoadBitmap(icon)
87       return
88     }
89     object : AsyncTask<Void?, Void?, Bitmap>() {
90       override fun doInBackground(vararg p0: Void?): Bitmap? {
91         return loadIconTask(iconUrl)
92       }
93 
94       override fun onPostExecute(result: Bitmap?) {
95         listener.onLoadBitmap(result)
96       }
97     }.execute()
98   }
99 
getIconFromCachenull100   private fun getIconFromCache(iconUrl: String?): Bitmap? {
101     return if (iconUrl == null || TextUtils.isEmpty(iconUrl)) {
102       BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
103     } else memoryCache[iconUrl]
104   }
105 
loadIconTasknull106   private fun loadIconTask(iconUrl: String?): Bitmap? {
107     return try {
108       // TODO: inject shared OkHttp client
109       val url = URL(iconUrl)
110       val connection = url.openConnection() as HttpURLConnection
111       connection.doInput = true
112       connection.connect()
113       val input = connection.inputStream
114       val bitmap = BitmapFactory.decodeStream(input)
115       val width = bitmap.width
116       val height = bitmap.height
117       if (width <= MAX_BITMAP_SIZE && height <= MAX_BITMAP_SIZE) {
118         memoryCache.put(iconUrl, bitmap)
119         return bitmap
120       }
121       val maxDimension = max(width, height)
122       val scaledWidth = width.toFloat() * MAX_BITMAP_SIZE / maxDimension
123       val scaledHeight = height.toFloat() * MAX_BITMAP_SIZE / maxDimension
124       val scaledBitmap =
125         Bitmap.createScaledBitmap(bitmap, scaledWidth.toInt(), scaledHeight.toInt(), true)
126       memoryCache.put(iconUrl, scaledBitmap)
127       scaledBitmap
128     } catch (e: IOException) {
129       EXL.e(TAG, e)
130       BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
131     } catch (e: Throwable) {
132       EXL.e(TAG, e)
133       BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
134     }
135   }
136 
getColorFromManifestnull137   fun getColorFromManifest(manifest: Manifest): Int {
138     val colorString = manifest.getPrimaryColor()
139     return if (colorString != null && ColorParser.isValid(colorString)) {
140       Color.parseColor(colorString)
141     } else {
142       R.color.colorPrimary
143     }
144   }
145 
isAnonymousExperiencenull146   fun isAnonymousExperience(manifest: Manifest): Boolean {
147     return try {
148       manifest.getScopeKey().startsWith(ANONYMOUS_SCOPE_KEY_PREFIX)
149     } catch (e: JSONException) {
150       false
151     }
152   }
153 
getLocalKernelManifestAndAssetRequestHeadersnull154   private fun getLocalKernelManifestAndAssetRequestHeaders(): ManifestAndAssetRequestHeaders = try {
155     val manifestAndAssetRequestHeaders = JSONObject(ExponentBuildConstants.getBuildMachineKernelManifestAndAssetRequestHeaders())
156     val manifest = manifestAndAssetRequestHeaders.getJSONObject("manifest")
157     val assetRequestHeaders = manifestAndAssetRequestHeaders.getJSONObject("assetRequestHeaders")
158     manifest.put(MANIFEST_IS_VERIFIED_KEY, true)
159     ManifestAndAssetRequestHeaders(Manifest.fromManifestJson(manifest), assetRequestHeaders)
160   } catch (e: JSONException) {
161     throw RuntimeException("Can't get local manifest: $e")
162   }
163 
getEmbeddedKernelManifestnull164   private fun getEmbeddedKernelManifest(): Manifest? = try {
165     val inputStream = context.assets.open(EMBEDDED_KERNEL_MANIFEST_ASSET)
166     val jsonString = IOUtils.toString(inputStream)
167     val manifest = JSONObject(jsonString)
168     manifest.put(MANIFEST_IS_VERIFIED_KEY, true)
169     Manifest.fromManifestJson(manifest)
170   } catch (e: Exception) {
171     KernelProvider.instance.handleError(e)
172     null
173   }
174 
getKernelManifestAndAssetRequestHeadersnull175   fun getKernelManifestAndAssetRequestHeaders(): ManifestAndAssetRequestHeaders {
176     val manifestAndAssetRequestHeaders: ManifestAndAssetRequestHeaders
177     val log: String
178     if (exponentSharedPreferences.shouldUseEmbeddedKernel()) {
179       log = "Using embedded Expo kernel manifest"
180       manifestAndAssetRequestHeaders = ManifestAndAssetRequestHeaders(getEmbeddedKernelManifest()!!, JSONObject())
181     } else {
182       log = "Using local Expo kernel manifest"
183       manifestAndAssetRequestHeaders = getLocalKernelManifestAndAssetRequestHeaders()
184     }
185     if (!hasShownKernelManifestLog) {
186       hasShownKernelManifestLog = true
187       EXL.d(TAG, log + ": " + manifestAndAssetRequestHeaders.manifest.toString())
188     }
189     return manifestAndAssetRequestHeaders
190   }
191 
192   companion object {
193     private val TAG = ExponentManifest::class.java.simpleName
194 
195     const val MANIFEST_STRING_KEY = "manifestString"
196     const val MANIFEST_SIGNATURE_KEY = "signature"
197     const val MANIFEST_ID_KEY = "id"
198     const val MANIFEST_NAME_KEY = "name"
199     const val MANIFEST_APP_KEY_KEY = "appKey"
200     const val MANIFEST_SDK_VERSION_KEY = "sdkVersion"
201     const val MANIFEST_IS_VERIFIED_KEY = "isVerified"
202     const val MANIFEST_ICON_URL_KEY = "iconUrl"
203     const val MANIFEST_BACKGROUND_COLOR_KEY = "backgroundColor"
204     const val MANIFEST_PRIMARY_COLOR_KEY = "primaryColor"
205     const val MANIFEST_ORIENTATION_KEY = "orientation"
206     const val MANIFEST_DEVELOPER_KEY = "developer"
207     const val MANIFEST_DEVELOPER_TOOL_KEY = "tool"
208     const val MANIFEST_PACKAGER_OPTS_KEY = "packagerOpts"
209     const val MANIFEST_PACKAGER_OPTS_DEV_KEY = "dev"
210     const val MANIFEST_BUNDLE_URL_KEY = "bundleUrl"
211     const val MANIFEST_REVISION_ID_KEY = "revisionId"
212     const val MANIFEST_PUBLISHED_TIME_KEY = "publishedTime"
213     const val MANIFEST_COMMIT_TIME_KEY = "commitTime"
214     const val MANIFEST_LOADED_FROM_CACHE_KEY = "loadedFromCache"
215     const val MANIFEST_SLUG = "slug"
216     const val MANIFEST_ANDROID_INFO_KEY = "android"
217     const val MANIFEST_KEYBOARD_LAYOUT_MODE_KEY = "softwareKeyboardLayoutMode"
218 
219     // Statusbar
220     const val MANIFEST_STATUS_BAR_KEY = "androidStatusBar"
221     const val MANIFEST_STATUS_BAR_APPEARANCE = "barStyle"
222     const val MANIFEST_STATUS_BAR_BACKGROUND_COLOR = "backgroundColor"
223     const val MANIFEST_STATUS_BAR_HIDDEN = "hidden"
224     const val MANIFEST_STATUS_BAR_TRANSLUCENT = "translucent"
225 
226     // NavigationBar
227     const val MANIFEST_NAVIGATION_BAR_KEY = "androidNavigationBar"
228     const val MANIFEST_NAVIGATION_BAR_VISIBLILITY = "visible"
229     const val MANIFEST_NAVIGATION_BAR_APPEARANCE = "barStyle"
230     const val MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR = "backgroundColor"
231 
232     // Notification
233     const val MANIFEST_NOTIFICATION_INFO_KEY = "notification"
234     const val MANIFEST_NOTIFICATION_ICON_URL_KEY = "iconUrl"
235     const val MANIFEST_NOTIFICATION_COLOR_KEY = "color"
236     const val MANIFEST_NOTIFICATION_ANDROID_MODE = "androidMode"
237     const val MANIFEST_NOTIFICATION_ANDROID_COLLAPSED_TITLE = "androidCollapsedTitle"
238 
239     // Debugging
240     const val MANIFEST_DEBUGGER_HOST_KEY = "debuggerHost"
241     const val MANIFEST_MAIN_MODULE_NAME_KEY = "mainModuleName"
242 
243     // Splash
244     const val MANIFEST_SPLASH_INFO_KEY = "splash"
245     const val MANIFEST_SPLASH_IMAGE_URL_KEY = "imageUrl"
246     const val MANIFEST_SPLASH_RESIZE_MODE_KEY = "resizeMode"
247     const val MANIFEST_SPLASH_BACKGROUND_COLOR_KEY = "backgroundColor"
248 
249     // Updates
250     const val MANIFEST_UPDATES_INFO_KEY = "updates"
251     const val MANIFEST_UPDATES_TIMEOUT_KEY = "fallbackToCacheTimeout"
252     const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_KEY = "checkAutomatically"
253     const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_ON_LOAD = "ON_LOAD"
254     const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_ON_ERROR = "ON_ERROR_RECOVERY"
255 
256     // Development client
257     const val MANIFEST_DEVELOPMENT_CLIENT_KEY = "developmentClient"
258     const val MANIFEST_DEVELOPMENT_CLIENT_SILENT_LAUNCH_KEY = "silentLaunch"
259     const val DEEP_LINK_SEPARATOR = "--"
260     const val DEEP_LINK_SEPARATOR_WITH_SLASH = "--/"
261     const val QUERY_PARAM_KEY_RELEASE_CHANNEL = "release-channel"
262     const val QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION = "runtime-version"
263     const val QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME = "channel-name"
264 
265     private const val MAX_BITMAP_SIZE = 192
266     private const val REDIRECT_SNIPPET = "exp.host/--/to-exp/"
267     private const val ANONYMOUS_SCOPE_KEY_PREFIX = "@anonymous/"
268     private const val EMBEDDED_KERNEL_MANIFEST_ASSET = "kernel-manifest.json"
269     private const val EXPONENT_SERVER_HEADER = "Exponent-Server"
270 
271     private var hasShownKernelManifestLog = false
272 
273     @Throws(JSONException::class)
normalizeManifestInPlacenull274     fun normalizeManifestInPlace(manifest: Manifest, manifestUrl: String) {
275       manifest.mutateInternalJSONInPlace(object : InternalJSONMutator {
276         override fun updateJSON(json: JSONObject) {
277           if (!json.has(MANIFEST_ID_KEY)) {
278             json.put(MANIFEST_ID_KEY, manifestUrl)
279           }
280           if (!json.has(MANIFEST_NAME_KEY)) {
281             json.put(MANIFEST_NAME_KEY, "My New Experience")
282           }
283           if (!json.has(MANIFEST_PRIMARY_COLOR_KEY)) {
284             json.put(MANIFEST_PRIMARY_COLOR_KEY, "#023C69")
285           }
286           if (!json.has(MANIFEST_ICON_URL_KEY)) {
287             json.put(
288               MANIFEST_ICON_URL_KEY,
289               "https://d3lwq5rlu14cro.cloudfront.net/ExponentEmptyManifest_192.png"
290             )
291           }
292           if (!json.has(MANIFEST_ORIENTATION_KEY)) {
293             json.put(MANIFEST_ORIENTATION_KEY, "default")
294           }
295         }
296       })
297     }
298   }
299 
300   init {
301     val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
302     // Use 1/16th of the available memory for this memory cache.
303     val cacheSize = maxMemory / 16
304     memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
sizeOfnull305       override fun sizeOf(key: String?, bitmap: Bitmap): Int {
306         return bitmap.byteCount / 1024
307       }
308     }
309   }
310 }
311