I have a problem: I'm writing a weather app, using retrofit 2.0. When I run the app on the emulator, everything works fine(API 24, 28, 29). But today I launched my app on a physical device (Galaxy A21s, version android 10) and the request to the server is not working. The request works onResponse() but it comes with response.body () = = null and response.is Successful == null. But everything works in the emulator!
Can you tell us what the problem is and how to solve it?
class DataProcessing {
private val retrofitImpl: RetrofitImpl = RetrofitImpl()
private val mainActivity = MainActivity()
internal fun sendRequest(townName:String, instance : DataProcessingCallback){
retrofitImpl.getRequest().showWeather(townName).enqueue(object : Callback<DateWeather> {
override fun onResponse(call: retrofit2.Call<DateWeather>, response: Response<DateWeather>) {
if (response.isSuccessful && response.body() != null) {
processingData(response.body(), null, instance)
} else
processingData(null, Throwable("ответ не получен"), instance)
}
override fun onFailure(call: Call<DateWeather>, t: Throwable) {
Log.d("Main", "onFailure")
processingData(null, t, instance)
}
})
}
private fun processingData(dateWeather:DateWeather?, error: Throwable?, instance : DataProcessingCallback){
if (dateWeather == null || error != null) {
Log.d("Egor", "error: ${error!!.message.toString()}")
instance.showToastText("Произошла ошибка \n Возможно вы неправильно ввели название населенного пункта")
} else {
if (dateWeather == null) Log.d("Main", "Loose")
else {
val string = dateWeather.weather.get(0).toString()
val size = string.length - 1
instance.onSuccessfulDataProcessed(string.subSequence(13, size).toString(), dateWeather.main.temp!!.toInt())
}
}
}
}
interface ShowWeather{
#GET("weather?&appid=(TOKEN)&units=metric")// there is a token here, I just deleted it when publishing, everything is fine with it
fun showWeather(#Query("q") town: String): Call<DateWeather>
}
class RetrofitImpl{
fun getRequest() : ShowWeather{
val retrofitBuilder = Retrofit.Builder()
.baseUrl("http://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofitBuilder.create(ShowWeather::class.java)
}
}
data class DateWeather(
val main: Main,
val weather : List<Weather>
)
data class Main(
val temp : Double?
)
data class Weather(
val main: String
)
At a base URL, you are trying with http unsecured connection. Can you check with "https://" instead of "http://"
val retrofitBuilder = Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build()
The error turned out to be elementary: on a physical device, I used a hint (T9) that returned the city. After the name of the city there was a gap and this was the error. trim () solved my problem.
#Kishore A, thank you so much for your help!
townName = editText.getText().trim().toString()
Related
So I am using the Google's API and for some reason, I'm getting a generic error:
E/Network: searchBooks: Failed Getting books
When it initially loads up, the hard coded query "android" shows up with a list of books associated with the book topic. But when I search up a different topic like "shoes" for example, the error shows up. Even when you hard code a different topic other than "android", it still shows the error. I have checked the API and it is working properly with the different query searches.
Here's the Retrofit Interface:
#Singleton
interface BooksApi {
#GET(BOOK_EP)
suspend fun getAllBooks(
//don't initialize the query, so that the whole api is available to the user
#Query("q") query: String
): Book
#GET("$BOOK_EP/{bookId}")
suspend fun getBookInfo(
#Path("bookId") bookId: String
): Item
}
The Repo
class BookRepository #Inject constructor(private val api: BooksApi) {
suspend fun getBooks(searchQuery: String): Resource<List<Item>> {
return try {
Resource.Loading(data = true)
val itemList = api.getAllBooks(searchQuery).items
if(itemList.isNotEmpty()) Resource.Loading(data = false)
Resource.Success(data = itemList)
}catch (exception: Exception){
Resource.Error(message = exception.message.toString())
}
}
suspend fun getBookInfo(bookId: String): Resource<Item>{
val response = try {
Resource.Loading(data = true)
api.getBookInfo(bookId)
}catch (exception: Exception){
return Resource.Error(message = "An error occurred ${exception.message.toString()}")
}
Resource.Loading(data = false)
return Resource.Success(data = response)
}
The ViewModel:
class SearchViewModel #Inject constructor(private val repository: BookRepository): ViewModel(){
var list: List<Item> by mutableStateOf(listOf())
var isLoading: Boolean by mutableStateOf(true)
init {
loadBooks()
}
private fun loadBooks() {
searchBooks("android")
}
fun searchBooks(query: String) {
viewModelScope.launch(Dispatchers.Default) {
if (query.isEmpty()){
return#launch
}
try {
when(val response = repository.getBooks(query)){
is Resource.Success -> {
list = response.data!!
if (list.isNotEmpty()) isLoading = false
}
is Resource.Error -> {
isLoading = false
Log.e("Network", "searchBooks: Failed Getting books", )
}
else -> {isLoading = false}
}
}catch (exception: Exception){
isLoading = false
Log.d("Network", "searchBooks: ${exception.message.toString()}")
}
}
}
}
I'll leave the project public so you guys can check it out for more of an understanding
Github Link: https://github.com/OEThe11/ReadersApp
P.S. you would have to create a login (takes 30 sec), but once you do, you'll have access to the app immediately.
This issue is occurring because of JsonSyntaxException java.lang.NumberFormatException while the JSON response is getting parsed from the API. This is because the averageRating field in the VolumeInfo data class is declared as Int but the response can contain floating point values.
If you change averageRating field type from Int to Double in the VolumeInfo data class, the exception would no longer occur.
I suggest you to debug your code in such cases.
I am using Spotify API to login user to the app. this is the interface i wrote per documentation:
interface API {
#GET("/authorize")
fun login(#Query("client_id") client_id:String,
#Query("response_type") response_type:String,
#Query("redirect_uri")redirect_uri:String,
#Query("scope") scope:String
):Call<LoginResult>
This is the response result data class:
data class LoginResult(
val code: String
)
And this is the login function:
fun login() {
val BASE_URL = "https://accounts.spotify.com"
val CLIENT_ID = "c6c23e3e2f604f9aa1780fe7504e73c6"
val REDIRECT_URI = "com.example.myapp://callback"
val retrofit: Retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
val service: API = retrofit.create(API::class.java)
val listCall: Call<LoginResult> =
service.login(CLIENT_ID, "code", REDIRECT_URI, "user-top-read")
listCall.enqueue(object : Callback<LoginResult> {
override fun onResponse(response: Response<LoginResult>?, retrofit: Retrofit?) {
if (response?.body() != null) {
Log.i("result!", response.body().code)
}
if(response?.body() == null){
Log.i("Code" , response!!.code().toString())
Log.i("Response! ", "null response body")
}
}
override fun onFailure(t: Throwable?) {
Log.e("Here", "it is")
Log.e("Error", t!!.message.toString())
}
})
}
But I am getting this error:
E/Here: it is
E/Error: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 2 column 1 path $
There are a lot of questions here about this particular error and I read all of them and tried to implement the suggested solutions, but none worked.
Any help would be appreciated.
[this is the mentioned documentation link]
(https://developer.spotify.com/documentation/general/guides/authorization/code-flow/)
I am new to test cases and I am trying to write test cases for the below code but I did not get success after trying several method. My main target is to cover code coverage of MaintenanceStatusResponseHandler.kt class. I am using mockito to write the test cases. I am already implemented jococo for code coverage but I am facing some issue to write a test cases. Please help me to write test cases of MaintenanceStatusResponseHandler class
Thanks in advance
internal class MaintenanceStatusResponseHandler {
public fun getMaintenanceResponse(voiceAiConfig : VoiceAiConfig):MaintenanceStatus{
val maintenanceStatus = MaintenanceStatus()
val retrofitRepository = RetrofitRepository()
val maintenanceUrl : String
val jwtToken : String
when (voiceAiConfig.server) {
BuildConfig.ENV_PRODUCTION_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_PRODUCTION
maintenanceUrl = BuildConfig.MAINTENANCE_PROD_URL
}
BuildConfig.ENV_STAGING_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_STAGING
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
else -> {
jwtToken = BuildConfig.JWT_TOKEN_SANDBOX
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
}
val header = "${VoiceAISDKConstant.JWT_TOKEN_PREFIX} $jwtToken"
retrofitRepository.getRetrofit(maintenanceUrl)
.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}
I am trying to use the Authenticator to handle 401 response. What I have done is
fun provideAccessTokenAuthenticator(
mainApiServiceHolder: MainApiServiceHolder,
preferences: SharedPreferences
) = object : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val accessToken = preferences.getString(ACCESS_TOKEN, null)
if (!isRequestWithAccessToken(response) || accessToken == null) {
return null
}
synchronized(this) {
val newAccessToken = preferences.getString(ACCESS_TOKEN, null)!!
// Access token is refreshed in another thread.
if (accessToken != newAccessToken) {
return newRequestWithAccessToken(response.request, newAccessToken)
}
// Need to refresh an access token
val refreshTokenResponse = runBlocking {
Log.d("zzzzzzzzzz", "refresh token is running")
mainApiServiceHolder.mainApiService?.refreshToken(
"refresh_token",
preferences.getString(REFRESH_TOKEN, null)!!,
AuthRepository.CLIENT_ID,
AuthRepository.CLIENT_SECRET
)
}
Log.d("zzzzzzzzzz", refreshTokenResponse?.body()?.access_token!!)
return if (refreshTokenResponse?.isSuccessful!!) {
Log.d("zzzzzzzzzz", "refresh token is successful")
newRequestWithAccessToken(
response.request,
refreshTokenResponse.body()?.access_token!!
)
} else {
Log.d("zzzzzzzzzz", "refresh token is unsuccessful")
response.request.newBuilder().header("Content-Type", "application/json").build()
}
}
}
Now, it gets called when there is a 401 response. The refresh token call is also fired (from Log). However, it never gets the result in the refreshTokenResponse and nothing happens after that. I think its a wrong way of using runBlock. The api is
#FormUrlEncoded
#POST("/api/auth/token/")
suspend fun refreshToken(
#Field("grant_type") grant_type: String,
#Field("refresh_token") refresh_token: String,
#Field("client_id") client_id: String,
#Field("client_secret") client_secret: String
): Response<LoginResponse>
Any help would be really appreciated. Thanks
In the Retrofit API, consider replacing your async runBlocking{} suspend fun with a synchronous Call. I had the most luck avoiding the use of coroutines inside the Authenticator.
I was having the same problem. The token request went straight into a black hole. The app froze. The request was never seen again. No error, no nothing.
But everywhere else in the app, the suspend fun came back just fine. From ViewModels, from WorkManager, it worked every time. But from the Authenticator, never. What was wrong with the Authenticator? What was special about the Authenticator that made it act this way?
Then I replaced the runBlocking{} coroutine with a straightforward Call. This time, the request came back and the token arrived without a fuss.
The way I got the API to work looked like this:
#FormUrlEncoded
#POST("token")
fun refreshTokenSync(
#Field("refresh_token") refreshToken: String,
): Call<RefreshMyTokenResponse>
Then, in the Authenticator:
val call = API.refreshTokenSync(refreshToken)
val response = call.execute().body()
I hope this helps someone else who ran into the same issue. You may receive a warning from Android Studio that this is an inappropriate blocking call. Ignore it.
Refresh token only once for multiple requests
Log out user if refreshToken failed
Log out if user gets an error after first refreshing
Queue all requests while token is being refreshed
https://github.com/hoc081098/Refresh-Token-Sample/blob/master/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt
class AuthInterceptor #Inject constructor(
private val userLocalSource: UserLocalSource,
private val apiService: Provider<ApiService>,
) : Interceptor {
private val mutex = Mutex()
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().also { Timber.d("[1] $it") }
if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) {
return chain.proceedWithToken(req, null)
}
val token =
runBlocking { userLocalSource.user().first() }?.token.also { Timber.d("[2] $req $it") }
val res = chain.proceedWithToken(req, token)
if (res.code != HTTP_UNAUTHORIZED || token == null) {
return res
}
Timber.d("[3] $req")
val newToken: String? = runBlocking {
mutex.withLock {
val user =
userLocalSource.user().first().also { Timber.d("[4] $req $it") }
val maybeUpdatedToken = user?.token
when {
user == null || maybeUpdatedToken == null -> null.also { Timber.d("[5-1] $req") } // already logged out!
maybeUpdatedToken != token -> maybeUpdatedToken.also { Timber.d("[5-2] $req") } // refreshed by another request
else -> {
Timber.d("[5-3] $req")
val refreshTokenRes =
apiService.get().refreshToken(RefreshTokenBody(user.refreshToken, user.username))
.also {
Timber.d("[6] $req $it")
}
val code = refreshTokenRes.code()
if (code == HTTP_OK) {
refreshTokenRes.body()?.token?.also {
Timber.d("[7-1] $req")
userLocalSource.save(
user.toBuilder()
.setToken(it)
.build()
)
}
} else if (code == HTTP_UNAUTHORIZED) {
Timber.d("[7-2] $req")
userLocalSource.save(null)
null
} else {
Timber.d("[7-3] $req")
null
}
}
}
}
}
return if (newToken !== null) chain.proceedWithToken(req, newToken) else res
}
private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response =
req.newBuilder()
.apply {
if (token !== null) {
addHeader("Authorization", "Bearer $token")
}
}
.removeHeader(CUSTOM_HEADER)
.build()
.let(::proceed)
}
I have the following class:
interface API
{
#GET("/data.json")
suspend fun fetchData() : MyResponse
companion object
{
private val BASE_URL = "http://10.0.2.2:8080/"
fun create(): MyAPI
{
val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create()
val retrofit = Retrofit.Builder()
.addConverterFactory( GsonConverterFactory.create( gson ) )
.baseUrl( BASE_URL )
.build()
return retrofit.create( MyAPI::class.java )
}
}
}
I'm calling it the following way:
val data = StadiumAPI.create().fetchData()
Log.d( "gotdata", data.toString() )
Everything works great, but now I want to handle errors, I'm trying to accomplish something like this:
Var response = StadiumAPI.create().fetchData()
when( response.state )
{
Success -> doSomethingWithTheData( response.data )
Error -> showError( response.error )
Processing -> showSpinner()
}
The main problem is that not only I would need to handle success/error (based on the HTTP status code and if GSON conversion was successful) but also handle exceptions (like network issues) and pass down them as Error to the response state as well as keep the automatic conversion with GSON without handling it manually.
I'm completely lost where to go from here. As far as I understood I need to create a custom data type in the retrofit response API which will "accept" the response and then I could manipulate its properties to produce the code structure above. Could you please point me in the right direction where should I go from here? Thanks!
----------- EDIT -------------------------------------------
I found out I can do what I'm trying the following way:
interface API
{
#GET("/data.json")
suspend fun fetchData() : ApiResponse<MyResponse>
....
}
And here is the ApiResponse:
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
Log.d( "networkdebug", "success: " + response.body().toString() )
return if(response.isSuccessful) {
Log.d( "networkdebug", "success: " + response.body().toString() )
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
Log.d( "networkdebug", "error: " + msg.toString() )
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
But for whatever reason the CompanionObject within the ApiResponse is not triggering at all, any hints on what I could be doing wrong? Thanks!