1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent.network 3 4 import android.content.Context 5 import host.exp.exponent.Constants 6 import host.exp.exponent.analytics.EXL 7 import okhttp3.* 8 import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 import okio.BufferedSource 10 import okio.buffer 11 import okio.source 12 import java.io.FileNotFoundException 13 import java.io.IOException 14 import java.net.MalformedURLException 15 import java.net.URI 16 import java.net.URISyntaxException 17 import java.net.URL 18 19 class ExponentHttpClient( 20 private val context: Context, 21 private val okHttpClientFactory: ExponentNetwork.OkHttpClientFactory 22 ) { 23 interface SafeCallback { onFailurenull24 fun onFailure(e: IOException) 25 fun onResponse(response: ExpoResponse) 26 fun onCachedResponse(response: ExpoResponse, isEmbedded: Boolean) 27 } 28 29 fun call(request: Request, callback: ExpoHttpCallback) { 30 okHttpClientFactory.getNewClient().newCall(request).enqueue(object : Callback { 31 override fun onFailure(call: Call, e: IOException) { 32 callback.onFailure(e) 33 } 34 35 @Throws(IOException::class) 36 override fun onResponse(call: Call, response: Response) { 37 callback.onResponse(OkHttpV1ExpoResponse(response)) 38 } 39 }) 40 } 41 callSafenull42 fun callSafe(request: Request, callback: SafeCallback) { 43 val uri = request.url.toString() 44 okHttpClientFactory.getNewClient().newCall(request).enqueue(object : Callback { 45 override fun onFailure(call: Call, e: IOException) { 46 tryForcedCachedResponse(uri, request, callback, null, e) 47 } 48 49 @Throws(IOException::class) 50 override fun onResponse(call: Call, response: Response) { 51 if (response.isSuccessful) { 52 callback.onResponse(OkHttpV1ExpoResponse(response)) 53 } else { 54 tryForcedCachedResponse(uri, request, callback, response, null) 55 } 56 } 57 }) 58 } 59 callDefaultCachenull60 fun callDefaultCache(request: Request, callback: SafeCallback) { 61 tryForcedCachedResponse( 62 request.url.toString(), 63 request, 64 object : SafeCallback { 65 override fun onFailure(e: IOException) { 66 call( 67 request, 68 object : ExpoHttpCallback { 69 override fun onFailure(e: IOException) { 70 callback.onFailure(e) 71 } 72 73 @Throws(IOException::class) 74 override fun onResponse(response: ExpoResponse) { 75 callback.onResponse(response) 76 } 77 } 78 ) 79 } 80 81 override fun onResponse(response: ExpoResponse) { 82 callback.onResponse(response) 83 } 84 85 override fun onCachedResponse(response: ExpoResponse, isEmbedded: Boolean) { 86 callback.onCachedResponse(response, isEmbedded) 87 // You are responsible for updating the cache! 88 } 89 }, 90 null, 91 null 92 ) 93 } 94 tryForcedCachedResponsenull95 fun tryForcedCachedResponse( 96 uri: String, 97 request: Request, 98 callback: SafeCallback, 99 initialResponse: Response?, 100 initialException: IOException? 101 ) { 102 val newRequest = request.newBuilder() 103 .cacheControl(CacheControl.FORCE_CACHE) 104 .header(ExponentNetwork.IGNORE_INTERCEPTORS_HEADER, "blah") 105 .build() 106 107 okHttpClientFactory.getNewClient().newCall(newRequest).enqueue(object : Callback { 108 override fun onFailure(call: Call, e: IOException) { 109 tryHardCodedResponse(uri, call, callback, initialResponse, initialException) 110 } 111 112 @Throws(IOException::class) 113 override fun onResponse(call: Call, response: Response) { 114 if (response.isSuccessful) { 115 callback.onCachedResponse(OkHttpV1ExpoResponse(response), false) 116 } else { 117 tryHardCodedResponse(uri, call, callback, initialResponse, initialException) 118 } 119 } 120 }) 121 } 122 tryHardCodedResponsenull123 private fun tryHardCodedResponse( 124 uri: String, 125 call: Call, 126 callback: SafeCallback, 127 initialResponse: Response?, 128 initialException: IOException? 129 ) { 130 try { 131 val normalizedUri = normalizeUri(uri) 132 for (embeddedResponse in Constants.EMBEDDED_RESPONSES) { 133 // We only want to use embedded responses once. After they are used they will be added 134 // to the OkHttp cache and we should use the version from that cache. We don't want a situation 135 // where we have version 1 of a manifest saved as the embedded response, get version 2 saved 136 // to the OkHttp cache, cache gets evicted, and we regress to version 1. Want to only use 137 // monotonically increasing manifest versions. 138 if (normalizedUri == normalizeUri(embeddedResponse.url)) { 139 val response = Response.Builder() 140 .request(call.request()) 141 .protocol(Protocol.HTTP_1_1) 142 .code(200) 143 .message("OK") 144 .body( 145 responseBodyForFile( 146 embeddedResponse.responseFilePath, 147 embeddedResponse.mediaType.toMediaTypeOrNull() 148 ) 149 ) 150 .build() 151 callback.onCachedResponse(OkHttpV1ExpoResponse(response), true) 152 return 153 } 154 } 155 } catch (e: Throwable) { 156 EXL.e(TAG, e) 157 } 158 159 when { 160 initialResponse != null -> callback.onResponse(OkHttpV1ExpoResponse(initialResponse)) 161 initialException != null -> callback.onFailure(initialException) 162 else -> callback.onFailure(IOException("No hard coded response found")) 163 } 164 } 165 responseBodyForFilenull166 private fun responseBodyForFile(assetsPath: String, contentType: MediaType?): ResponseBody? { 167 return try { 168 var strippedAssetsPath = assetsPath 169 if (strippedAssetsPath.startsWith("assets://")) { 170 strippedAssetsPath = strippedAssetsPath.substring("assets://".length) 171 } 172 173 val stream = context.assets.open(strippedAssetsPath) 174 val source = stream.source() 175 val buffer = source.buffer() 176 177 object : ResponseBody() { 178 override fun contentType(): MediaType? { 179 return contentType 180 } 181 182 override fun contentLength(): Long { 183 return -1 184 } 185 186 override fun source(): BufferedSource { 187 return buffer 188 } 189 } 190 } catch (e: FileNotFoundException) { 191 EXL.e(TAG, e) 192 null 193 } catch (e: IOException) { 194 EXL.e(TAG, e) 195 null 196 } 197 } 198 199 companion object { 200 private val TAG = ExponentHttpClient::class.java.simpleName 201 normalizeUrinull202 private fun normalizeUri(uriString: String): String { 203 return try { 204 val url = URL(uriString) 205 var port = url.port 206 if (port == -1) { 207 if (url.protocol == "http") { 208 port = 80 209 } else if (url.protocol == "https") { 210 port = 443 211 } 212 } 213 val uri = URI(url.protocol, url.userInfo, url.host, port, url.path, url.query, url.ref) 214 uri.toString() 215 } catch (e: MalformedURLException) { 216 uriString 217 } catch (e: URISyntaxException) { 218 uriString 219 } 220 } 221 } 222 } 223