How to get Context inside an Object? - android

I am using retrofit to fetch some data and for that I am passing a token in Header for Authentication.
I want to fetch the token from the Shared Preferences in my Retrofit Client Object but I don't know how to?
I tried to get a context in the object using a function but then it gives me WARNING that
Do not place Android context classes in static fields (static reference to RetrofitClient which has field context pointing to Context); this is a memory leak (and also breaks Instant Run) less...
Also i tried to get context in my interface of retrofit and I got the context without warning but I don't know where to get Shared Preferences.
interface Api {
var context:Context;
#FormUrlEncoded
#POST("getMerchantProductsSlideContent")
fun getProductsForSlide(
//Don't know how to get value from shared refercne to this header
#Header("Authentication: Bearer ")
#Field("token") token:String,
#Field("deviceId") deviceId:String,
#Field("content_receiver") content_receiver:String,
#Field("content_type") content_type:String,
#Field("data") data:Array<String>
):Call<DefaultResponse>
fun getContext(mContext:Context){
context = mContext
}
}
This is retrofitClient.kt
object RetrofitClient {
private val AUTH = "Bearer $token"
private const val BASE_URL = "http://192.168.1.5/Projects/Sitapuriya/public/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
.addHeader("Authorization", AUTH)
.method(original.method(), original.body())
val request = requestBuilder.build()
chain.proceed(request)
}.build()
val instance: Api by lazy{
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
retrofit.create(Api::class.java)
}
}
This is my retrofit interface
interface Api {
#FormUrlEncoded
#POST("getMerchantProductsSlideContent")
fun getProductsForSlide(
#Field("token2") token2:String,
#Field("deviceId") deviceId:String,
#Field("content_receiver") content_receiver:String,
#Field("content_type") content_type:String,
#Field("data") data:Array<String>
):Call<DefaultResponse>
}
[UPDATED] This is my activity n which i am calling the retrofit
val data = arrayOf(merchantId)
RetrofitClient.instance.getContext(this)
RetrofitClient.instance.getProductsForSlide(
token,
deviceId,
"MERCHANT",
"MERCHANT_VIEW_BASIC",
data
).enqueue(object:Callback<DefaultResponse>{
override fun onFailure(call: Call<DefaultResponse>, t: Throwable) {
Toast.makeText(applicationContext,"ERROR: ${t.message}",Toast.LENGTH_LONG).show()
}
override fun onResponse(
call: Call<DefaultResponse>,
response: retrofit2.Response<DefaultResponse>
) {
Toast.makeText(applicationContext,"SUCCESS: ${response.body()?.content}",Toast.LENGTH_LONG).show()
}
})
I want to get the token from Shared Preferences and use it as a header for my request and I know to access Shared Preferences we need a context. How can I get the context in Object?
[UPDATE-2] Tried #Blundell answer
interface Api {
var token: String
#FormUrlEncoded
#POST("getMerchantProductsSlideContent")
fun getProductsForSlide(
#Header("Authentication: Bearer $token")
#Field("token") token:String,
#Field("deviceId") deviceId:String,
#Field("content_receiver") content_receiver:String,
#Field("content_type") content_type:String,
#Field("data") data:Array<String>
):Call<DefaultResponse>
fun setAuthHeader(token2:String){
token = token2
}
}
But it gives error: An annotation argument must be a compile-time constant

Try to get token in your activity (you can use activity's context and get token from shared preferences) and pass this token to your retrofit class.
Also try to read something about dependency injection, dagger2, koin etc to provide different dependencies to your classes
interface Api {
#FormUrlEncoded
#POST("getMerchantProductsSlideContent")
fun getProductsForSlide(
#Header("Authentication") token:String,
#Field("deviceId") deviceId:String,
#Field("content_receiver") content_receiver:String,
#Field("content_type") content_type:String,
#Field("data") data:Array<String>
):Call<DefaultResponse>
}
In your activity:
val prefToken = // get it from prefences
val token = "Bearer " + prefToken

Instead of trying to store the context in a singleton, store the header you want to send. Access the context & sharedpreferences in your Activity.
Change:
RetrofitClient.instance.getContext(this)
To something like
RetrofitClient.instance.setAuthHeader(getSharedPreferences().getString("Header"))

Related

Android - Retrofit2 Authorization with Bearer Token stored in SharedPreferences

I'm trying to use Retrofit2 in Android app, I'd like to add Bearer Token to http request header which stored with SharedPreferences.
But the below code return the error when launch app.
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Context android.content.Context.getApplicationContext()' on a null object reference
object RetrofitInstance : Application() {
private const val API_BASE_URL = BuildConfig.API_BASE_URL
private val sharedPreferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(
applicationContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
}
private val httpBuilder: OkHttpClient.Builder
get() {
val httpClient = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val original = chain.request()
val bearerToken = sharedPreferences.getString(
applicationContext.getString(R.string.preference_token_data_key),
null
)
val request = original.newBuilder()
.header("Accept", "application/json")
.header("Authorization", "Bearer $bearerToken")
.build()
return#Interceptor chain.proceed(request)
})
return httpClient
}
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(httpBuilder.build())
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
)
)
.addCallAdapterFactory(ResultCallAdapterFactory())
.build()
}
val api: TestService by lazy {
retrofit.create(TestService::class.java)
}
}
Is there any way to use the value stored with SharedPreferences in Singleton Object?
The Application class in your Android App is Singleton; what you need to do is change the object to class, add this class to the AndroidManifest.xml
like that android:name=".RetrofitInstance", and then get access to that class in your activities or fragments like the following
(application as RetrofitInstance).api
But I don't think this is the right place for such a thing, usually we create separate files that handle the creation and configuration for retrofit, a separate class that wraps the SharedPreferences, and have another implementation entity that integrates these two (get the data from shared preferences and pass them to the Retrofit OkHttpBuilder).

Use Dagger to inject different Retrofit and OkHttp object before and after login

I have my Retrofit and OkHttp instances injected. I am changing the headers of my OkHttp object using an Interceptor. In this Interceptor, I set information such as app name, version etc.
I am now adding a login and want to set an authToken to the headers. However, this only exists AFTER login, obviously. So what I want to do is be able to add a new header after login for authToken. However, I can't get access to my OkHttp object as its buried in my module, and I just get my RestClient object injected in my class, with everything else built.
I was thinking of copying all my provides... code and making duplicates for before an after Login. For example, wrap my RestClient in a class called "PreLoginApi" that doesn't have authToken, and then creating another wrapper class called "PostLoginApi" which has the authToken, but this won't work if Dagger creates the objects on app launch.
#Provides
#Singleton
#Named("HeaderInterceptor")
fun provideHeaderInterceptor(context: Context): Interceptor {
val headerMap = context.getHeaderMap()
return Interceptor { chain ->
val original = chain.request()
val builder = original.newBuilder()
headerMap.keys.forEach {
builder.addHeader(it, headerMap[it] ?: "")
}
val request = builder.build()
chain.proceed(request)
}
}
#Provides
fun providesOkHttpClient(
#Named("HeaderInterceptor") headerInterceptor: Interceptor,
): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.addInterceptor(headerInterceptor)
return builder.build()
}
You can see here where I call my extension function Context.getHeaderMap(). Inside this function, I'm retrieving the authToken from EncryptedSharedPreferences.
So ideally, I'd like to be able to tell Dagger to redo this initialisation once I login, because at this stage the authToken will be set. Is it possible to just re-initialise this one module or will I have to wrap my RestClient in another class, and maybe use Lazy loading to initialise the "PostLoginApi"?
Thanks.
EDIT:
I do similar for Firebase FCM token, where I pass in the headerMap during initialisation, like this:
private fun updateFirebaseToken(headerMap: HashMap<String, String>) {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
return#OnCompleteListener
}
// Get new FCM registration token
task.result?.let { token ->
headerMap["FirebaseId"] = token
}
})
}
But I don't think I can do something like this for a SharedPrefs value.

Retrofit2 authentication error to IBM's Speech to Text

I am trying to access IBM's Speech to Text service without using the library. I am using Retrofit with GSON.
The issue is in the authentication, which apparently does not occur correctly, returning code 401. From the official documentation, the HTTP request should come in this format
curl -X POST -u "apikey:{apikey}" \
--header "Content-Type: audio/flac" \
--data-binary #{path_to_file}audio-file.flac \
"{url}/v1/recognize"
When I test the curl command with my credentials, the service works fine.
This is the interface I'm using
interface SpeechToTextApi {
#Multipart
#POST("v1/recognize")
fun speechToText(
#Header("Authorization") authKey: String,
#Part("file") filename: RequestBody,
#Part voiceFile: MultipartBody.Part
): Call<List<SpeechToText>>
}
where I have the following data classes
data class SpeechToText(val results: List<SttResult>)
data class SttResult(val alternatives: List<RecognitionResult>, val final: Boolean)
data class RecognitionResult(val confidence: Float, val transcript: String)
and this is how I set up Retrofit
private val retrofit = Retrofit.Builder()
.baseUrl(STT_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val service = retrofit.create(SpeechToTextApi::class.java)
while calling the actual service looks like this
val requestFile = RequestBody.create(MediaType.parse("audio/mp3"), file.name)
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
service
.speechToText(getString(R.string.stt_iam_api_key), requestFile, body)
.enqueue(object: Callback<List<SpeechToText>> {
override fun onResponse(call: Call<List<SpeechToText>>, response: Response<List<SpeechToText>>) {
val listOfStts = response.body()
Log.d(TAG, "Response code: ${response.code()}")
if (listOfStts != null) {
for (stt in listOfStts) {
for (res in stt.results) {
Log.d(TAG, "Final value: ${res.final}")
for (alt in res.alternatives) {
Log.d(TAG, "Alternative confidence: ${alt.confidence}\nTranscript: ${alt.transcript}")
Toast.makeText(this#MainActivity, alt.transcript, Toast.LENGTH_SHORT).show()
}
}
}
}
}
override fun onFailure(call: Call<List<SpeechToText>>, t: Throwable) {
Log.d(TAG, "Error: ${t.message}")
t.printStackTrace()
}
})
Recordings are MP3 files, for which I am sure they are stored correctly and accessible. I have replaced audio/flac with audio/mp3 as well.
Issue seems to be in the way authentication works. Prior to the code I have shown above, I've used
private val retrofit = Retrofit.Builder()
.baseUrl(STT_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
val headers = request
.headers()
.newBuilder()
.add("Authorization", getString(R.string.stt_iam_api_key))
.build()
val finalRequest = request.newBuilder().headers(headers).build()
chain.proceed(finalRequest)
}
.build())
.build()
but the same response code 401 persisted. Of course, the interface method lacked the #Header parameter.
Any sort of help is much appreciated.
I am kind of saddened by the fact nobody was able to solve this one sooner, but here's the solution I came across by accident when working on a different project altogether.
As you can see from the curl command, authentication comes in the form of username: password pattern, in this case, username being apikey string and password is your API key.
So the way you should tackle this is by building your Retrofit instance this way:
fun init(token: String) {
//Set logging interceptor to BODY and redact Authorization header
interceptor.level = HttpLoggingInterceptor.Level.BODY
interceptor.redactHeader("Authorization")
//Build OkHttp client with logging and token interceptors
val okhttp = OkHttpClient().newBuilder()
.addInterceptor(interceptor)
.addInterceptor(TokenInterceptor(token))
.build()
//Set field naming policy for Gson
val gsonBuilder = GsonBuilder()
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
//Build Retrofit instance
retrofit = Retrofit.Builder()
.baseUrl(IBM_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gsonBuilder.create()))
.client(okhttp)
.build()
}
and create this custom interceptor
class TokenInterceptor constructor(private val token: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val requestBuilder = original
.newBuilder()
.addHeader("Authorization", Credentials.basic("apikey", token))
.url(original.url)
return chain.proceed(requestBuilder.build())
}
}
You need to use Credentials.basic() in order to encode credentials.
I really hope somebody with a similar issue stumbles across this and saves themselves some time.

Kotlin : Okhttpclient correct way of creating single instance? OkHttpClient doesn't go through server ip check?

I'm new at android kotlin development and currently trying to solve how to correctly create a single instance of OkHttpClient for app-wide usage. I've currently sort-of* created a single instance of client and using it to communicate with the server, however currently the back-end server is not using token/userid for validation but IP check. I can log in the user no problem, but after going to another activity trying to call api, I'm being blocked access by server because apparently IP is not the same. I've used POSTMAN as well as already created a same functioning iOS app that is working with no issue. So my question is am i creating the single instance of OkHttpClient wrong? Or is OkHttpClient not suitable for this kind of ipcheck system? Should i use other library, and if yes, any suggestion and examples?
Thanks in advance
Currently i tried creating it like this :
class MyApplication: Application(){
companion object{
lateinit var client: OkHttpClient
}
override fun onCreate(){
super.onCreate()
client = OkHttpClient()
}
}
Then i created a helper class for it :
class OkHttpRequest {
private var client : OkHttpClient = MyApplication.client
fun POST(url: String, parameters: HashMap<String, String>, callback: Callback): Call {
val builder = FormBody.Builder()
val it = parameters.entries.iterator()
while (it.hasNext()) {
val pair = it.next() as Map.Entry<*, *>
builder.add(pair.key.toString(), pair.value.toString())
}
val formBody = builder.build()
val request = Request.Builder()
.url(url)
.post(formBody)
.build()
val call = client.newCall(request)
call.enqueue(callback)
return call
}
fun GET(url: String, callback: Callback): Call {
val request = Request.Builder()
.url(url)
.build()
val call = client.newCall(request)
call.enqueue(callback)
return call
}
}
Finally I'm using it like this :
val loginUrl = MyApplication.postLoginUrl
var userIdValue = user_id_textfield.text.toString()
var passwordValue = password_textfield.text.toString()
val map: HashMap<String, String> = hashMapOf("email" to userIdValue, "password" to passwordValue)
var request = OkHttpRequest()
request.POST(loginUrl, map, object : Callback {
val responseData = response.body?.string()
// do something with response Data
}
And on another activity after user log in :
val getPaidTo = MyApplication.getPaidTo
var request = OkHttpRequest()
request.GET(getPaidTo, object: Callback{
//do something with data
}
First, don't use your OkHttpClient directly in every Activity or Fragment, use DI and move all of your business logic into Repository or some source of data.
Here I will share some easy way to make REST request with Retrofit, OkHttpClient and Koin, if you want use the same:
WebServiceModule:
val webServiceModule = module {
//Create HttpLoggingInterceptor
single { createLoggingInterceptor() }
//Create OkHttpClient
single { createOkHttpClient(get()) }
//Create WebServiceApi
single { createWebServiceApi(get()) }
}
/**
* Setup a Retrofit.Builder and create a WebServiceApi instance which will hold all HTTP requests
*
* #okHttpClient Factory for HTTP calls
*/
private fun createWebServiceApi(okHttpClient: OkHttpClient): WebServiceApi {
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.REST_SERVICE_BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
return retrofit.create(WebServiceApi::class.java)
}
/**
* Create a OkHttpClient which is used to send HTTP requests and read their responses.
*
* #loggingInterceptor logging interceptor
*/
private fun createOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.readTimeout(defaultTimeout, TimeUnit.SECONDS)
.connectTimeout(defaultTimeout, TimeUnit.SECONDS)
.build()
}
And now you can inject your WebServiceApi everywhere, but better inject it in your Repository and then use it from some ViewModel
ViewModelModule:
val viewModelModule = module {
//Create an instance of MyRepository
single { MyRepository(webServiceApi = get()) }
}
Hope this help somehow
Okay, after i check with the back-end developer, i figured out the problem wasn't the ip address(it stays the same) but that the cookie was not saved by okhttp, both POSTMan and xcode automatically save the token returned into cookie so i never noticed that was the problem. So after googling a-bit, the solution can be as easy as this:
class MyApplication : Application(){
override fun onCreate(){
val cookieJar = PersistentCookieJar(SetCookieCache(),SharedPrefsCookiePersistor(this))
client = OkHttpClient.Builder()
.cookieJar(cookieJar)
.build()
}
}
With adding persistentCookieJar to gradle.

should I store user object with authentication token attribute in SharedPreferences?

It seems general consensus recommends storing authentication token in SharedPreferences, as this post suggests. However, I have a user object with several attributes, including an id, email, name, authentication token, and possibly more attributes. Should I store the authentication token ALONE in SharedPreferences and then for each activity, find the user by the authentication token:
String authenticationToken = User.findByAuthenticationToken(sharedPreferences.getString("authenticationToken"));
or should I convert the object to JSON and then store the entire user object in SharedPreferences? And then for each activity, deserialize it. This seems less efficient.
You can store token in SharedPrefs, or use DI, as #Francesc said.
If you are using retrofit, I advice you to create singleton (or module) with retrofit instance, and add request interceptor to OkHttpClient.
private OkHttpClient buildClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//do any another stuff
builder.addInterceptor(new RequestAuthInterceptor());
return builder.build();
}
public static class RequestAuthInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
String jwtToken = SharedPrefs.getKeyJwt();
if (jwtToken != null) {
Request.Builder builder = chain.request().newBuilder();
builder.addHeader("Authorization", jwtToken);
return chain.proceed(builder.build());
} else {
return chain.proceed(chain.request());
}
}
}
Storing that data in preferences simply to pass it from one activity to another is inefficient. If you are using Dagger2 or any other dependency injection framework, you could consider having a User module that is created when the user logs in and holds the relevant user information. You can then access this module from your activities and read the user info.
If you're not using dependency injection you can do something similar with a Singleton class that you would populate when logging in and clear when logging out.
Edit: here are some extracts from one of my apps (this is in Kotlin)
Retrieving the token for API requests:
val httpClientBuilder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
httpClientBuilder.addInterceptor(logging)
}
httpClientBuilder.addInterceptor { chain ->
var request = chain.request()
val token = ObjectGraphController.userComponent?.token
token?.apply {
request = request.newBuilder().addHeader("Session-Id", this).build()
}
chain.proceed(request)
}
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(<YOUR ENDPOINT>)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io()))
.client(httpClientBuilder.build())
.build()
User module:
#Module
class UserModule(val token: String) {
#Provides
#UserScope
fun providesToken() = token
}
Set/clear component when logging in and out:
object ObjectGraphController {
lateinit var objectGraph: ObjectGraph
var userComponent: UserComponent? = null
private set
fun setUserComponent(token: String?) {
if (token != null) {
userComponent = objectGraph.userComponent(UserModule(token))
} else {
userComponent = null
}
}
}

Categories

Resources