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