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