Send push notification via Firebase from android device - android

I'm interested to send push notification via Firebase from android app.
Firebase documentation states that payload json must be sent to https://fcm.googleapis.com/fcm/send
So I using with Retrofit to send a json but the request fails, and if I sending a request without Retrofit it's works perfect.
What's the difference between the tow implementations and why it's not works with Retrofit.
Here are the two implementations and the error when using Retrofit.
val notificationBody = NotificationBody(notificationData, "/topics/all")
val rb = Gson().toJson(notificationBody).toRequestBody("application/json".toMediaTypeOrNull())
val responeNoti = RetrofitManager.notificationServiceApi.postNotification(rb)
responeNoti.enqueue(object : Callback<ResponseBody>{
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
Log.d(TAG, t.message)
}
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
Log.d(TAG, response.toString())
}
})
NotificationServiceApi
private const val FCM_URL = "https://fcm.googleapis.com/fcm/"
val notificationServiceApi: ServiceApi by lazy{
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClientBuilder = OkHttpClient.Builder()
okHttpClientBuilder.addInterceptor(object : Interceptor{
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request: Request = chain.request().newBuilder().
addHeader("Content-Type", "application/json").
addHeader("Authorization","key=*my firebase key*").build()
return chain.proceed(request)
}
}).addInterceptor(interceptor)
val notificationRetrofit = Retrofit.Builder()
.client(okHttpClientBuilder.build())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(FCM_URL)
.build()
notificationRetrofit.create(ServiceApi::class.java)
}
ServiceApi
#POST("send/")
fun postNotification(#Body notificationBody: RequestBody) : Call<ResponseBody>
The log Http
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: --> POST
https://fcm.googleapis.com/fcm/send/
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: Content-
Length: 117
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: Content-Type: application/json
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: Authorization: key=my firebase key
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: {"data":{"imageUrl":"https://i.imgur.com/KA1jrU7.jpg","textBody":"my content","title":"my title"},"to":"/topics/all"}
2020-09-06 00:12:27.991 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: --> END POST (117-byte body)
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: <-- 404 https://fcm.googleapis.com/fcm/send/ (1206ms)
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: content-type: text/plain; charset=utf-8
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: x-content-type-options: nosniff
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: x-frame-options: SAMEORIGIN
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: x-xss-protection: 0
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: date: Sat, 05 Sep 2020 21:12:29 GMT
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: content-length: 135
2020-09-06 00:12:29.198 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
2020-09-06 00:12:29.199 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: A valid push subscription endpoint should be specified in the URL as such: https://fcm.googleapis.com/wp/dHIoDxE7Hdg:APA91bH1Zj0kNa...
2020-09-06 00:12:29.199 32059-32244/com.bagi.soreknetmanager I/okhttp.OkHttpClient: <-- END HTTP (135-byte body)
2020-09-06 00:12:29.201 32059-32059/com.bagi.soreknetmanager D/MainActivity: Response{protocol=h2, code=404, message=, url=https://fcm.googleapis.com/fcm/send/}
The implementation that works great without using Retrofit
val notificationBody = NotificationBody(notificationData, "/topics/all")
val jsonPayload = createJsonFromObject(notificationBody)
val thread = Thread(Runnable {
try {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
var client = OkHttpClient().newBuilder().addInterceptor(interceptor)
.build()
var mediaType = "application/json".toMediaTypeOrNull()
var body: RequestBody = RequestBody.create(
mediaType,
jsonPayload
)
var request: Request = Request.Builder()
.url("https://fcm.googleapis.com/fcm/send")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.addHeader(
"Authorization",
"key=my firebase key"
)
.build()
var response: okhttp3.Response = client.newCall(request).execute()
} catch (e: Exception) {
e.printStackTrace()
}
})
thread.start()

Error is in your URL..
Check your FCM_URL, it is incomplete, you forgot to append "send" word
FCM_URL = "https://fcm.googleapis.com/fcm/"
Correct URL-
FCM_URL ="https://fcm.googleapis.com/fcm/send"

Related

Retrofit - multipart

I would like to upload image to api. I have headers for request and also headers for Part with files:
#Multipart
#POST("/api")
suspend fun uploadFile(
#Part("payload") file: RequestBody,
#Header("Mobile-Client-Type") mobileClientType: String,
#Header("Mobile-Client-Version") version: String,
#Header("Authorization") authorization: String,
): retrofit2.Response<Any>
and
internal class RetrofitFileUpload {
private val fileUploadService = FileUploadService.create()
suspend fun requestWithPayload(
urlString: String,
file: File,
myHeaders:
pl.probs.uploadapp.jsonObjects.Headers,
payload: Payload
): retrofit2.Response<Any> {
val mediaType = "multipart/form-data".toMediaTypeOrNull()
val fileBody = RequestBody.create(mediaType, file)
val body: RequestBody = MultipartBody.Builder().addPart(
Headers.headersOf(
"type",
payload.type,
"identifier",
payload.identifier,
"omit_package_creation",
payload.omitPackageCreation.toString(),
"file_path",
payload.filePath.toString()
), fileBody
).build()
return fileUploadService.uploadFile(
body,
myHeaders.mobileClientType,
myHeaders.mobileClientVersion,
myHeaders.authorization,
)
}
}
Result looks like I have 2 parts instead of one:
Content-Type: multipart/form-data; boundary=eac4d249-196d-421e-9ffe-923b472a5f58
....other headers...........
--eac4d249-196d-421e-9ffe-923b472a5f58
Content-Disposition: form-data; name="payload"
Content-Transfer-Encoding: binary
Content-Type: multipart/mixed; boundary=55d4b971-d171-42a7-a82a-5deb1bccac98
Content-Length: 2421
--55d4b971-d171-42a7-a82a-5deb1bccac98
type: myType
identifier: myID
omit_package_creation: true
file_path: /data/0...
Content-Type: multipart/form-data
Content-Length: 2072
{[Content from file]}
--55d4b971-d171-42a7-a82a-5deb1bccac98--
WHat should I change to have only 1 part?

send get request to open_elevation.com with kotlin and retrofit

I want to get altitude from api service and:
base url is https://api.open-elevation.com/
end point is api/v1/lookup?
and get two args lat and long like this
https://api.open-elevation.com/api/v1/lookup?locations=42,35
output is a json
How can i send get request and get data and please help
Hm. unfortunately you can't send a request with varargs through retrofit. But, you can do it like this.
Do something like this..
lifecycleScope.launch {
val latLong = arrayOf(41.161758, -8.583933)
val result = withContext(Dispatchers.IO) {
RetroClient.getInstance().lookup(latLong.joinToString())
}.awaitResponse()
if (result.isSuccessful && result.code() == 200) {
// If success do your sutff here
}
}
RetroClient
object RetroClient {
private val httpInterceptor by lazy {
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
}
private val client by lazy {
OkHttpClient.Builder()
.addInterceptor(httpInterceptor)
.callTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
private val gson by lazy {
GsonBuilder().setLenient().create()
}
fun getInstance(): RetroInterface {
return Retrofit.Builder()
.baseUrl("https://api.open-elevation.com/api/v1/")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson)).build()
.create(RetroInterface::class.java)
}
}
RetroInterface
interface RetroInterface {
#GET("lookup")
fun lookup(
#Query("locations") locations: String,
): Call<LocationsModel?>
}
LocationsModel
data class LocationsModel(
var results: List<Result>?
)
data class Result(
var elevation: Int?,
var latitude: Double?,
var longitude: Double?
)
RESULTS
I/okhttp.OkHttpClient: <-- 200 OK https://api.open-elevation.com/api/v1/lookup?locations=41.161758%2C%20-8.583933 (1411ms)
I/okhttp.OkHttpClient: Server: nginx/1.21.1
I/okhttp.OkHttpClient: Date: Sat, 27 Aug 2022 18:33:55 GMT
I/okhttp.OkHttpClient: Content-Type: application/json
I/okhttp.OkHttpClient: Transfer-Encoding: chunked
I/okhttp.OkHttpClient: Connection: keep-alive
I/okhttp.OkHttpClient: Access-Control-Allow-Origin: *
I/okhttp.OkHttpClient: Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS
I/okhttp.OkHttpClient: Access-Control-Allow-Headers: Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token
I/okhttp.OkHttpClient: Strict-Transport-Security: max-age=31536000; includeSubDomains
I/okhttp.OkHttpClient: {"results": [{"latitude": 41.161758, "longitude": -8.583933, "elevation": 117}]}
I/okhttp.OkHttpClient: <-- END HTTP (80-byte body)
Hope you understand. - Thank you.

Upload image from Uri to Retrofit (S3 Signed url) not working

Uploading to S3 signed URL's with a file as an origin works without problems, but trying to do it with an Uri I get from my ActivityResult is not working for me.
Here is what I try:
I'm using a InputStreamRequestBody class like described here: https://commonsware.com/blog/2020/07/05/multipart-upload-okttp-uri.html
class InputStreamRequestBody(
private val contentType: MediaType,
private val contentResolver: ContentResolver,
private val uri: Uri
) : RequestBody() {
override fun contentType() = contentType
override fun contentLength(): Long = -1
#Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
val input = contentResolver.openInputStream(uri)
input?.use { sink.writeAll(it.source()) }
?: throw IOException("Could not open $uri")
}
}
My Retrofit UploadAPI interface:
interface UploadAPI {
#PUT
suspend fun uploadFile(
#Header("x-amz-acl") contentType: String,
#Url uploadUrl: String,
#Body file: RequestBody
): Response<Unit>
companion object {
const val BASE_URL = "https://....com"
}
}
And here my Retrofit Singleton in AppModule:
#Provides
#Singleton
fun provideUploadApi(): UploadAPI {
return Retrofit.Builder()
.baseUrl(UploadAPI.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(UploadAPI::class.java)
}
And then calling the InputStreamRequestBody class with my Uri (in my repository) and calling the retrofit instance and upload function:
val resolver = context.contentResolver
val type = ("image/jpeg").toMediaType()
val contentPart = InputStreamRequestBody(type, resolver, content)
uploadAPI.uploadFile(
"public-read",
mySignedUrl,
contentPart
)
this is what my logginInterceptor tells me:
2022-05-19 22:16:48.844 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: --> PUT https://app-content.ams3.digitaloceanspaces.com/Files/Test/profilbild.webp?Content-Type=image%2Fwebp&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=CENSORED-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read
2022-05-19 22:16:48.844 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: Content-Type: image/jpeg
2022-05-19 22:16:48.844 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: x-amz-acl: public-read
2022-05-19 22:16:48.896 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: --> END PUT (binary -1-byte body omitted)
2022-05-19 22:16:49.503 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: <-- 411 Length Required https://app-content.ams3.digitaloceanspaces.com/Files/Test/profilbild.webp?Content-Type=image%2Fwebp&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=CENSORED-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read (606ms)
2022-05-19 22:16:49.504 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: content-length: 239
2022-05-19 22:16:49.504 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: x-amz-request-id: tx000000000000004541b27-00622f61-20f93ecc-ams3c
2022-05-19 22:16:49.505 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: accept-ranges: bytes
2022-05-19 22:16:49.505 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: content-type: application/xml
2022-05-19 22:16:49.505 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: date: Thu, 19 May 2022 15:16:49 GMT
2022-05-19 22:16:49.505 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: cache-control: max-age=60
2022-05-19 22:16:49.506 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: strict-transport-security: max-age=15552000; includeSubDomains; preload
2022-05-19 22:16:49.506 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: connection: close
2022-05-19 22:16:49.506 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: <?xml version="1.0" encoding="UTF-8"?><Error><Code>MissingContentLength</Code><BucketName>app-content</BucketName><RequestId>tx00000000000000fffb27-0062865f61-20f93ecc-ams3c</RequestId><HostId>20f93ecc-ams3c-ams3-zg03</HostId></Error>
2022-05-19 22:16:49.507 8469-8703/com.havanasun.loginplayground I/okhttp.OkHttpClient: <-- END HTTP (239-byte body)
I have the strong feeling that I cannot just pass the instance of InputStreamRequestBody as a requestBody eventhough this class inherrits from RequestBody.

Unable to get API response with Retrofit and coroutines

I am trying to call a post API with Retrofit using coroutine but after hours of searching I am unable to find the mistake. There is no clear information about the error.
Below is the simple JSON I should be getting after posting the data.
{
"status": "OK",
"data": 5,
"message": null,
"code": "200"
}
api call from fragment
lifecycleScope.launchWhenStarted {
sharedPreferences.getString(AppConstant.token, "")?.let { authToken ->
viewModelSubProperty.sendDetail(authToken,propertyData)
}
}
where propertyData is the values I am send to the api.
ViewModel
suspend fun sendDetail(token: String,data: SubPropertyData) {
viewModelScope.launch(Dispatchers.IO) {
val responseResult = repository.sendDetail(token = token,data)
withContext(Dispatchers.Main) {
if(responseResult is Result.Success) {
Log.e("Viewmodel", "${responseResult.data}")
Log.e("Viewmodel", "$responseResult")
}
else if (responseResult is Result.Error && responseResult.exception.message == AppConstant.INTERNET_ERR_MSG)
EventBus.getDefault().post(WebServiceErrorEvent(null, true))
else if (responseResult is Result.Error)
EventBus.getDefault().post(WebServiceErrorEvent(responseResult))
}
}
}
DataSource
suspend fun sendDetail(token: String, data: SubPropertyData): Result<AddUpdateDataResponse> {
val serverResponse: Response<AddUpdateDataResponse>
return try {
serverResponse = serverApi.sendDetail(token,data)
if(serverResponse.isSuccessful)
Result.Success(serverResponse.body()!!)
else {
Log.e("what-code ", ""+serverResponse.errorBody())
Log.e("what-code ", serverResponse.errorBody()?.charStream().toString())
Result.Success(serverResponse.body()!!)
}
} catch (e: Throwable) {
if(e is NoConnectivityException)
Result.Error(IOException(AppConstant.INTERNET_ERR_MSG))
else
Result.Error(IOException(e.localizedMessage))
}
}
Api Call
#POST("api/addOrUpdateSubjectPropertyDetail")
suspend fun sendDetail(
#Header("Authorization") Authorization:String,
#Body data: SubPropertyData
) : Response<AddUpdateDataResponse>
Pojo class
data class AddUpdateDataResponse(
#SerializedName("code")
val code: String?,
#SerializedName("data")
val data: Int?,
#SerializedName("message")
val message: String? = null,
#SerializedName("status")
val status: String?
)
Logs
I/okhttp.OkHttpClient: --> POST https://abc/api/AddOrUpdateSubjectPropertyDetail
Content-Type: application/json; charset=UTF-8
Content-Length: 398
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOiI0IiwiaHR0cDovL3NjaGVtYXnzK5gv0k4
I/okhttp.OkHttpClient: {"address":{"city":"Karachi","countryId":1,"countryName":"Pak","countyId":1,"countyName ":"SSS","stateId":11,"stateName":"Sindh","street":"akl","unit":"00","zipCode":"123"},"appraisedPropertyValue":100000.0,"floodInsurance":0.0,"homeOwnerInsurance":200.0,"isMixedUseProperty":true,"loanApplicationId":5,"mixedUsePropertyExplanation":"ashh","occupancyTypeId":1,"propertyTax":100.0,"propertyTypeId":1}
--> END POST (398-byte body)
--> END POST (398-byte body)
I/okhttp.OkHttpClient: <-- 500
https://abc/api/AddOrUpdateSubjectPropertyDetail (167ms)
I/okhttp.OkHttpClient: server: Microsoft-IIS/10.0
I/okhttp.OkHttpClient: x-powered-by: ASP.NET
date: Mon, 08 Nov 2021 22:31:31 GMT
I/okhttp.OkHttpClient: <-- END HTTP (0-byte body)
E/what-code: null
null
okhttp3.ResponseBody$BomAwareReader#3554188

Retrofit2 and OkHttp3 use cache only when error occur such as Network errors or quota limit reach

I need to implement a catch when an error occur like network related or the API limit has been reached. Now I've seen a lot of good samples yet it seems they are missing something.
This tutorial the handling of cache is based the network status like if a mobile data or Wi-Fi of the device is on/off. It is not the solution since one can be connected to a network but the network has no data or no internet at all.
The ideal flow on me would be
Cache every time a fetch is success which repeats every 5 seconds.
Use only the last cached data available if any of the performed fetch
failed, if no error then use the fresh data set from online response.
The cached data can be available for days or weeks and will only be
updated again every new fetch is success.
If no cache yet is available and the very first fetch was failed only then show the error.
My code
interface EndpointServices {
companion object {
private fun interceptor(): Interceptor {
return Interceptor { chain ->
var request: Request = chain.request()
val originalResponse: Response = chain.proceed(request)
val cacheControl: String? = originalResponse.header("Cache-Control")
if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
cacheControl.contains("must-revalidate") || cacheControl.contains("max-stale=0")
) {
Log.wtf("INTERCEPT", "SAVE A CACHE")
val cc: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.build()
request = request.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public")
.cacheControl(cc)
.build()
chain.proceed(request)
} else {
Log.wtf("INTERCEPT", "ONLINE FETCH")
originalResponse.newBuilder()
.removeHeader("Pragma")
.build()
}
}
}
private fun onlineOfflineHandling(): Interceptor {
return Interceptor { chain ->
try {
Log.wtf("INTERCEPT", "TRY ONLINE")
chain.proceed(chain.request())
} catch (e: Exception) {
Log.wtf("INTERCEPT", "FALLBACK TO CACHE")
val cacheControl: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.onlyIfCached() //Caching condition
.build()
val offlineRequest: Request = chain.request().newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(offlineRequest)
}
}
}
fun create(baseUrl: String, context: Context): EndpointServices {
val cacheSize: Long = 10 * 1024 * 1024 // 10 MB
val cache = Cache(context.cacheDir, cacheSize)
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor())
.addInterceptor(onlineOfflineHandling())
.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create()
)
.addConverterFactory(
MoshiConverterFactory.create()
)
.client(httpClient)
.baseUrl(baseUrl)
.build()
return retrofit.create(EndpointServices::class.java)
}
}
Main activity
intervalDisposable = Observable.interval(0L, 5L, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.d("Interval", it.toString())
fetchAssets(UriUtil.assetField, "30")
}
private fun fetchAssets(field: String, limit: String) {
disposable = EndpointServices.create(url, requireContext()).getAssetItems(
field,
limit
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
//Our response here
},
{ error ->
//Error, offline and no cache has been found
Log.wtf("WTF", "${error.message}")
Toast.makeText(context, error.message, Toast.LENGTH_LONG).show()
}
)
}
I am working with #GET
UPDATES Apr 10 2021
I tried to work with Youtube API as an example and this is what the test result.
Mobile Data/Wi-Fi off
INTERCEPT: TRY ONLINE
INTERCEPT: FALLBACK TO CACHE
The response was return (it works!)
Wi-Fi on and connected to a network but has no data/internet connection service
INTERCEPT: TRY ONLINE
Waiting for response to timeout?
INTERCEPT: FALLBACK TO CACHE
No response was return (WTF?)
Mobile data/Wi-Fi on and internet service is available (Online) works on Youtube API only so far
INTERCEPT: TRY ONLINE
INTERCEPT: ONLINE FETCH
The response was return (it works!)
I tried working on other APIs too but no luck so far only YouTube API works but not as intended yet. I need an approach that could work on almost any API.
UPDATES Apr 11 2021
I updated the code and somewhat managed to make it work almost to what we needed.
interface EndpointServices {
companion object {
private fun interceptor(): Interceptor {
return Interceptor { chain ->
val request: Request = chain.request()
val originalResponse: Response = chain.proceed(request)
val cacheControlStatus: String? = originalResponse.header("Cache-Control")
if (cacheControlStatus == null || cacheControlStatus.contains("no-store") || cacheControlStatus.contains(
"no-cache") ||
cacheControlStatus.contains("must-revalidate") || cacheControlStatus.contains("max-stale=0")
) {
Log.wtf("INTERCEPT", "ORIGINAL CACHE-CONTROL: $cacheControlStatus")
} else {
Log.wtf("INTERCEPT",
"ORIGINAL : CACHE-CONTROL: $cacheControlStatus")
}
Log.wtf("INTERCEPT",
"OVERWRITE CACHE-CONTROL: ${request.cacheControl} | CACHEABLE? ${
CacheStrategy.isCacheable(originalResponse,
request)
}")
originalResponse.newBuilder()
.build()
}
}
private fun onlineOfflineHandling(): Interceptor {
return Interceptor { chain ->
try {
Log.wtf("INTERCEPT", "FETCH ONLINE")
val cacheControl = CacheControl.Builder()
.maxAge(5, TimeUnit.SECONDS)
.build()
val response = chain.proceed(chain.request().newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, $cacheControl")
.build())
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")
response
} catch (e: Exception) {
Log.wtf("INTERCEPT", "FALLBACK TO CACHE ${e.message}")
val cacheControl: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.onlyIfCached() // Use Cache if available
.build()
val offlineRequest: Request = chain.request().newBuilder()
.cacheControl(cacheControl)
.build()
val response = chain.proceed(offlineRequest)
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")
response
}
}
}
fun create(baseUrl: String, context: Context): EndpointServices {
// Inexact 150 MB of maximum cache size for a total of 4000 assets where about 1MB/30 assets
// The remaining available space will be use for other cacheable requests
val cacheSize: Long = 150 * 1024 * 1024
val cache = Cache(context.cacheDir, cacheSize)
Log.wtf("CACHE DIRECTORY", cache.directory.absolutePath)
for (cacheUrl in cache.urls())
Log.wtf("CACHE URLS", cacheUrl)
Log.wtf("CACHE OCCUPIED/TOTAL SIZE", "${cache.size()} ${cache.maxSize()}")
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor())
.addInterceptor(onlineOfflineHandling())
.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create()
)
.addConverterFactory(
MoshiConverterFactory.create()
)
.client(httpClient)
.baseUrl(baseUrl)
.build()
return retrofit.create(EndpointServices::class.java)
}
}
#GET("search")
fun getVideoItems(
#Query("key") key: String,
#Query("part") part: String,
#Query("maxResults") maxResults: String,
#Query("order") order: String,
#Query("type") type: String,
#Query("channelId") channelId: String,
):
Single<VideoItemModel>
}
MainActivity
EndpointServices.create(url, requireContext()).getVideoItems(
AppUtils.videoKey,
"id,snippet",
"20",
"date",
"video",
channelId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
Log.wtf("RESPONSE", result.toString())
adapter.submitList(result.videoData)
swipeRefreshLayout.isRefreshing = false
logTxt.text = null
},
{ error ->
Log.wtf("WTF", "${error.message}")
swipeRefreshLayout.isRefreshing = false
if (adapter.currentList.isEmpty() || (error is HttpException && error.code() == HttpURLConnection.HTTP_GATEWAY_TIMEOUT)){
adapter.submitList(mutableListOf())
logTxt.text = getString(R.string.swipeToRefresh)
}
}
)
FLOW BASED ON LOGS
WHEN ONLINE
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: ORIGINAL : CACHE-CONTROL: private
A/INTERCEPT: OVERWRITE CACHE-CONTROL: public, max-age=5 | CACHEABLE? true
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK Response{protocol=h2, code=304, message=, url=https://api.com}
A/RESPONSE: VideoItemModel(.....) WORKING!
COMPLETELY OFFLINE (Wi-Fi/Mobile Data OFF)
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK null
A/RESPONSE: VideoItemModel(.....) WORKING!
JUST CONNECTED TO A NETWORK BUT REALLY NO INTERNET SERVICE (Wi-Fi/Mobile Data ON)
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
???WHERE IS THE CALLBACK JUST LIKE THE PREVIOUS ONE???
Also worth mentioning that neither of the line
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}") is being called on this last scenario.
Not an answer yet, but a standalone example that might help debug this. From the example you've provided it's hard to tell if there is a logic problem in your code.
#!/usr/bin/env kotlin
#file:Repository("https://repo1.maven.org/maven2/")
#file:DependsOn("com.squareup.okhttp3:okhttp:4.9.1")
#file:DependsOn("com.squareup.okhttp3:logging-interceptor:4.9.1")
#file:CompilerOptions("-jvm-target", "1.8")
// https://stackoverflow.com/a/66364994/1542667
import okhttp3.Cache
import okhttp3.Dns
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.LoggingEventListener
import java.io.File
import java.net.InetAddress
import java.net.UnknownHostException
val cache = Cache(File("/tmp/tmpcache.2"), 100 * 1024 * 1024)
val systemDns = Dns.SYSTEM
var failDns = false
val client = OkHttpClient.Builder()
.eventListenerFactory(LoggingEventListener.Factory { println(it) })
.cache(cache)
.dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> {
if (failDns) {
throw UnknownHostException("NO HOST")
}
return systemDns.lookup(hostname)
}
})
.build()
val request = Request.Builder()
.url("https://raw.github.com/square/okhttp/master/README.md")
.build()
makeRequest()
failDns = true
makeRequest()
fun makeRequest() {
client.newCall(request).execute().use {
println(it.headers)
println("cache ${it.cacheResponse} network ${it.networkResponse}")
println(it.body!!.string().lines().first())
}
}
This example will run directly in Intellij or from the command line if you have kotlin installed. https://stackoverflow.com/a/66364994/1542667
Output
[0 ms] callStart: Request{method=GET, url=https://raw.github.com/square/okhttp/master/README.md}
[19 ms] cacheMiss
[20 ms] proxySelectStart: https://raw.github.com/
[21 ms] proxySelectEnd: [DIRECT]
[21 ms] dnsStart: raw.github.com
[64 ms] dnsEnd: [raw.github.com/185.199.109.133, raw.github.com/185.199.110.133, raw.github.com/185.199.111.133, raw.github.com/185.199.108.133]
[73 ms] connectStart: raw.github.com/185.199.109.133:443 DIRECT
[105 ms] secureConnectStart
[293 ms] secureConnectEnd: Handshake{tlsVersion=TLS_1_3 cipherSuite=TLS_AES_256_GCM_SHA384 peerCertificates=[CN=www.github.com, O="GitHub, Inc.", L=San Francisco, ST=California, C=US, CN=DigiCert SHA2 High Assurance Server CA, OU=www.digicert.com, O=DigiCert Inc, C=US, CN=DigiCert High Assurance EV Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US] localCertificates=[]}
[365 ms] connectEnd: h2
[368 ms] connectionAcquired: Connection{raw.github.com:443, proxy=DIRECT hostAddress=raw.github.com/185.199.109.133:443 cipherSuite=TLS_AES_256_GCM_SHA384 protocol=h2}
[369 ms] requestHeadersStart
[372 ms] requestHeadersEnd
[721 ms] responseHeadersStart
[723 ms] responseHeadersEnd: Response{protocol=h2, code=301, message=, url=https://raw.github.com/square/okhttp/master/README.md}
[726 ms] responseBodyStart
[726 ms] responseBodyEnd: byteCount=0
[771 ms] cacheMiss
[771 ms] connectionReleased
[771 ms] proxySelectStart: https://raw.githubusercontent.com/
[772 ms] proxySelectEnd: [DIRECT]
[772 ms] dnsStart: raw.githubusercontent.com
[797 ms] dnsEnd: [raw.githubusercontent.com/185.199.111.133, raw.githubusercontent.com/185.199.108.133, raw.githubusercontent.com/185.199.109.133, raw.githubusercontent.com/185.199.110.133]
[799 ms] connectionAcquired: Connection{raw.github.com:443, proxy=DIRECT hostAddress=raw.github.com/185.199.109.133:443 cipherSuite=TLS_AES_256_GCM_SHA384 protocol=h2}
[799 ms] requestHeadersStart
[800 ms] requestHeadersEnd
[980 ms] responseHeadersStart
[980 ms] responseHeadersEnd: Response{protocol=h2, code=200, message=, url=https://raw.githubusercontent.com/square/okhttp/master/README.md}
cache-control: max-age=300
content-security-policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
content-type: text/plain; charset=utf-8
etag: W/"846e6af5d55b29262841dbd93b02a95ff38f8709b68aa782be13f29d094a5421"
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
x-frame-options: deny
x-xss-protection: 1; mode=block
x-github-request-id: F08E:5C09:CAD563:D4D764:6072B974
accept-ranges: bytes
date: Sun, 11 Apr 2021 09:52:07 GMT
via: 1.1 varnish
x-served-by: cache-lon4280-LON
x-cache: HIT
x-cache-hits: 1
x-timer: S1618134728.761197,VS0,VE155
vary: Authorization,Accept-Encoding
access-control-allow-origin: *
x-fastly-request-id: da78b4491988420875d181584295baef3b3f3a6d
expires: Sun, 11 Apr 2021 09:57:07 GMT
source-age: 0
cache null network Response{protocol=h2, code=200, message=, url=https://raw.githubusercontent.com/square/okhttp/master/README.md}
[1007 ms] responseBodyStart
[1007 ms] responseBodyEnd: byteCount=2747
[1007 ms] connectionReleased
[1007 ms] callEnd
OkHttp
[0 ms] callStart: Request{method=GET, url=https://raw.github.com/square/okhttp/master/README.md}
[15 ms] cacheConditionalHit: Response{protocol=http/1.1, code=301, message=, url=https://raw.github.com/square/okhttp/master/README.md}
[15 ms] connectionAcquired: Connection{raw.github.com:443, proxy=DIRECT hostAddress=raw.github.com/185.199.109.133:443 cipherSuite=TLS_AES_256_GCM_SHA384 protocol=h2}
[15 ms] requestHeadersStart
[15 ms] requestHeadersEnd
[35 ms] responseHeadersStart
[35 ms] responseHeadersEnd: Response{protocol=h2, code=301, message=, url=https://raw.github.com/square/okhttp/master/README.md}
[35 ms] responseBodyStart
[35 ms] responseBodyEnd: byteCount=0
[42 ms] cacheMiss
[52 ms] cacheHit: Response{protocol=http/1.1, code=200, message=, url=https://raw.githubusercontent.com/square/okhttp/master/README.md}
[52 ms] connectionReleased
[52 ms] callEnd
cache-control: max-age=300
content-security-policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
content-type: text/plain; charset=utf-8
etag: W/"846e6af5d55b29262841dbd93b02a95ff38f8709b68aa782be13f29d094a5421"
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
x-frame-options: deny
x-xss-protection: 1; mode=block
x-github-request-id: F08E:5C09:CAD563:D4D764:6072B974
accept-ranges: bytes
date: Sun, 11 Apr 2021 09:52:07 GMT
via: 1.1 varnish
x-served-by: cache-lon4280-LON
x-cache: HIT
x-cache-hits: 1
x-timer: S1618134728.761197,VS0,VE155
vary: Authorization,Accept-Encoding
access-control-allow-origin: *
x-fastly-request-id: da78b4491988420875d181584295baef3b3f3a6d
expires: Sun, 11 Apr 2021 09:57:07 GMT
source-age: 0
cache Response{protocol=http/1.1, code=200, message=, url=https://raw.githubusercontent.com/square/okhttp/master/README.md} network null
OkHttp

Categories

Resources