I am using GRPC with proto in my project and I have KEY and AUTHORITY tokens to access the server API.
So, I need to update KEY using my AUTHORITY.
I am building Channel like this:
OkHttpChannelBuilder.forAddress(host, port)
.usePlaintext()
.intercept(auth, logger)
.build()
My interceptor looks like:
class AuthClientInterceptor(
private val prefs: Preferences,
private val keyApi: KeyApi) : ClientInterceptor {
companion object {
private const val ACCESS_TOKEN = "authorization"
}
override fun <ReqT : Any?, RespT : Any?> interceptCall(method: MethodDescriptor<ReqT, RespT>?,
callOptions: CallOptions?,
next: Channel): ClientCall<ReqT, RespT> {
val call = next.newCall(method, callOptions)
val callForwarding = object : ClientInterceptors.CheckedForwardingClientCall<ReqT, RespT>(call) {
override fun checkedStart(responseListener: Listener<RespT>?, headers: Metadata) {
synchronized(this#AuthClientInterceptor) {
val keyCreated = prefs.getAccessKeyCreated()
val keyExpires = prefs.getAccessKeyExpires()
val currentTime = System.currentTimeMillis()
if (currentTime < keyCreated || currentTime > keyExpires) {
keyApi.issueNewKey(prefs.getAuthority())
.map { it.data }
.doOnSuccess { prefs.setAccessKey(it.token) }
.doOnSuccess { prefs.setAccessKeyCreated(it.createdDate) }
.doOnSuccess { prefs.setAccessKeyExpires(it.expiresDate) }
.blockingGet()
}
}
val keyData = Metadata.Key.of(ACCESS_TOKEN, Metadata.ASCII_STRING_MARSHALLER)
if (headers[keyData] == null) {
headers.put(keyData, "Bearer ${prefs.getAccessKey()}")
}
call.start(responseListener, headers)
}
}
return callForwarding
}
}
As you can see, I just check current time and compare it with token created and expiry dates.
So, I don't like that way. I want implement this:
1) Send request to server;
2) Check response. If it means, that my KEY expired, refresh key synchronously and repeat the request (like authenticator).
But I didn't find the solution or any helpful information about implementing this with gRPC. Can somebody help me?
Here is the full client interceptor class you can use.
class Interceptor() : ClientInterceptor {
override fun <ReqT : Any?, RespT : Any?> interceptCall(method: MethodDescriptor<ReqT, RespT>?, callOptions: CallOptions?, next: Channel?): ClientCall<ReqT, RespT> {
return object : ClientCall<ReqT, RespT>() {
var listener: Listener<RespT>? = null
var metadata: Metadata? = null
var message: ReqT? = null
var request = 0
var call: ClientCall<ReqT, RespT>? = null
override fun start(responseListener: Listener<RespT>?, headers: Metadata?) {
this.listener = responseListener
this.metadata = headers
}
override fun sendMessage(message: ReqT) {
assert(this.message == null)
this.message = message
}
override fun request(numMessages: Int) {
request += numMessages
assert(this.message == null)
}
override fun isReady(): Boolean {
return false
}
override fun halfClose() {
startCall(object : ForwardingClientCallListener<RespT>() {
var delegate: Listener<RespT>? = null
override fun onReady() {
delegate = listener
super.onReady()
}
override fun delegate(): Listener<RespT> {
if (delegate == null) {
throw IllegalStateException()
}
return delegate!!
}
override fun onClose(status: Status?, trailers: Metadata?) {
if (delegate == null) {
super.onClose(status, trailers)
return
}
if (!needToRetry(status, trailers)) {
delegate = listener
super.onClose(status, trailers)
return
}
startCall(listener) // Only retry once
}
private fun needToRetry(status: Status?, trailers: Metadata?): Boolean {
if (status?.code?.toStatus() == UNAUTHENTICATED) {
Log.e("code", status?.code.toString())
return true
}
return false
}
})
}
private fun startCall(listener: Listener<RespT>?) {
call = next?.newCall(method, callOptions)
val headers = Metadata()
headers.merge(metadata)
call?.start(listener, headers)
assert(this.message != null)
call?.request(request)
call?.sendMessage(message)
call?.halfClose()
}
override fun cancel(message: String?, cause: Throwable?) {
if (call != null) {
call?.cancel(message, cause)
}
listener?.onClose(Status.CANCELLED.withDescription(message).withCause(cause), Metadata())
}
}
}
}
It buffers the message and retries, you can add your logic in needToRetry(status, trailers)
For more information, you can visit this GitHub link.
If you want to do that, I think you will have to handle it at the application level exactly as you described. This is because gRPC does not know about your application level tokens.
Make the RPC
Notice that the RPC failed due to an expired token
Call your code that refreshes the token
Repeat
What is the authenticator that you are referring to?
Related
I have a sharedViewModel with LiveData properties.
MainActivity:
private val logViewModel by viewModels<LogViewModel>()
fun startLocationUpdates() {
val locationItem = LocationItem(...)
logViewModel.addLogEntry(locationItem)
}
override fun onCreate(savedInstanceState: Bundle?) {
val observer = object : FileObserver(applicationContext.filesDir.path + "/" + jsonFileName) {
override fun onEvent(event: Int, file: String?) {
if (event == FileObserver.MODIFY) {
loadLogViewModel()
}
}
}
observer.startWatching()
}
fun loadLogViewModel() {
val json = getData(this)
val myType = object : TypeToken<ArrayList<LocationItem>>() {}.type
if (json != null) {
val listItems: ArrayList<LocationItem> = gson.fromJson(json, myType)
for (item in listItems) {
logViewModel.addLogEntry(item)
}
}
}
LogFragment:
private val logViewModel: LogViewModel by activityViewModels()
logViewModel.locationListItems.observe(requireActivity()) {
locationItemAdapter.updateLocations()
}
LogViewModel:
class LogViewModel : ViewModel() {
var locationListItems: MutableLiveData<LocationItem> = MutableLiveData<LocationItem>()
fun addLogEntry(locationItem: LocationItem) {
locationListItems.postValue(locationItem)
}
}
When I execute startLocationUpdates() it all works as expected. The observer gets notifications.
But when the logfile (json) is changed through the WorkManager (potentially within a different process/thread) and the FileObserver kicks in to load the JSON file and adds them to the LogViewModel, the Live data is not firing anything.
Why does it make a difference if the fileObserver is doing it, or if startLocationUpdates() would be doing it? I can only assume this is a threading issue. But I thought LivaData deals with that?
The question about post requests in android has been asked before, but all the solutions I've tried have not worked properly. On top of that, a lot of them seem to be overly complicated as well. All I wish to do is make a post to a specific sight with a few body parameters. Is there any simple way to do that?
Let me explain my request calling structure using Retrofit.
build.gradle(app)
// Retrofit + GSON
implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
ApiClient.kt
object ApiClient {
private const val baseUrl = ApiInterface.BASE_URL
private var retrofit: Retrofit? = null
private val dispatcher = Dispatcher()
fun getClient(): Retrofit? {
val logging = HttpLoggingInterceptor()
if (BuildConfig.DEBUG)
logging.level = HttpLoggingInterceptor.Level.BODY
else
logging.level = HttpLoggingInterceptor.Level.NONE
if (retrofit == null) {
retrofit = Retrofit.Builder()
.client(OkHttpClient().newBuilder().readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS).retryOnConnectionFailure(false)
.dispatcher(
dispatcher
).addInterceptor(Interceptor { chain: Interceptor.Chain? ->
val newRequest = chain?.request()!!.newBuilder()
return#Interceptor chain.proceed(newRequest.build())
}).addInterceptor(logging).build()
)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
ApiClient will be used to initialize Retrofit singleton object, also initialize logging interceptors so you can keep track of the requests and responses in the logcat by using the keyword 'okhttp'.
SingleEnqueueCall.kt
object SingleEnqueueCall {
var retryCount = 0
lateinit var snackbar: Snackbar
fun <T> callRetrofit(
activity: Activity,
call: Call<T>,
apiName: String,
isLoaderShown: Boolean,
apiListener: IGenericCallBack
) {
snackbar = Snackbar.make(
activity.findViewById(android.R.id.content),
Constants.CONST_NO_INTERNET_CONNECTION, Snackbar.LENGTH_INDEFINITE
)
if (isLoaderShown)
activity.showAppLoader()
snackbar.dismiss()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
hideAppLoader()
if (response.isSuccessful) {
retryCount = 0
apiListener.success(apiName, response.body())
} else {
when {
response.errorBody() != null -> try {
val json = JSONObject(response.errorBody()!!.string())
Log.e("TEGD", "JSON==> " + response.errorBody())
Log.e("TEGD", "Response Code==> " + response.code())
val error = json.get("message") as String
apiListener.failure(apiName, error)
} catch (e: Exception) {
e.printStackTrace()
Log.e("TGED", "JSON==> " + e.message)
Log.e("TGED", "Response Code==> " + response.code())
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
}
else -> {
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
return
}
}
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
hideAppLoader()
val callBack = this
if (t.message != "Canceled") {
Log.e("TGED", "Fail==> " + t.localizedMessage)
if (t is UnknownHostException || t is IOException) {
snackbar.setAction("Retry") {
snackbar.dismiss()
enqueueWithRetry(activity, call, callBack, isLoaderShown)
}
snackbar.show()
apiListener.failure(apiName, Constants.CONST_NO_INTERNET_CONNECTION)
} else {
retryCount = 0
apiListener.failure(apiName, t.toString())
}
} else {
retryCount = 0
}
}
})
}
fun <T> enqueueWithRetry(
activity: Activity,
call: Call<T>,
callback: Callback<T>,
isLoaderShown: Boolean
) {
activity.showAppLoader()
call.clone().enqueue(callback)
}
}
SingleEnqueueCall will be used for calling the retrofit, it is quite versatile, written with onFailure() functions and by passing Call to it, we can call an API along with ApiName parameter so this function can be used for any possible calls and by ApiName, we can distinguish in the response that which API the result came from.
Constants.kt
object Constants {
const val CONST_NO_INTERNET_CONNECTION = "Please check your internet
connection"
const val CONST_SERVER_NOT_RESPONDING = "Server not responding!
Please try again later"
const val USER_REGISTER = "/api/User/register"
}
ApiInterface.kt
interface ApiInterface {
companion object {
const val BASE_URL = "URL_LINK"
}
#POST(Constants.USER_REGISTER)
fun userRegister(#Body userRegisterRequest: UserRegisterRequest):
Call<UserRegisterResponse>
}
UserRegisterRequest.kt
data class UserRegisterRequest(
val Email: String,
val Password: String
)
UserRegisterResponse.kt
data class UserRegisterResponse(
val Message: String,
val Code: Int
)
IGenericCallBack.kt
interface IGenericCallBack {
fun success(apiName: String, response: Any?)
fun failure(apiName: String, message: String?)
}
MyApplication.kt
class MyApplication : Application() {
companion object {
lateinit var apiService: ApiInterface
}
override fun onCreate() {
super.onCreate()
apiService = ApiClient.getClient()!!.create(ApiInterface::class.java)
}
}
MyApplication is the application class to initialize Retrofit at the launch of the app.
AndroidManifest.xml
android:name=".MyApplication"
You have to write above tag in AndroidManifest inside Application tag.
MainActivity.kt
class MainActivity : AppCompatActivity(), IGenericCallBack {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val call = MyApplication.apiService.userRegister(UserRegisterRequest(email, password))
SingleEnqueueCall.callRetrofit(this, call, Constants.USER_REGISTER, true, this)
}
override fun success(apiName: String, response: Any?) {
val model = response as UserRegisterResponse
}
override fun failure(apiName: String, message: String?) {
if (message != null) {
showToastMessage(message)
}
}
}
Firstly, we create a call object by using the API defined in ApiInterface and passing the parameters (if any). Then using SingleEnqueueCall, we pass the call to the retrofit along with ApiName and the interface listener IGenericCallBack by using this. Remember to implement it to respective activity or fragment as above.
Secondly, you will have the response of the API whether in success() or failure() function overriden by IGenericCallBack
P.S: You can differentiate which API got the response by using the ApiName parameter inside success() function.
override fun success(apiName: String, response: Any?) {
when(ApiName) {
Constants.USER_REGISTER -> {
val model = response as UserRegisterResponse
}
}
}
The whole concept is to focus on reusability, now every API call has to create a call variable by using the API's inside ApiInterface then call that API by SingleEnqueueCall and get the response inside success() or failure() functions.
I am trying to write an Espresso test while I am using Paging library v2 and RxJava :
class PageKeyedItemDataSource<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val compositeDisposable: CompositeDisposable,
private val context : Context
) : PageKeyedDataSource<Int, Character>() {
private var isNext = true
private val isNetworkAvailable: Observable<Boolean> =
Observable.fromCallable { context.isNetworkAvailable() }
override fun fetchItems(page: Int): Observable<PeopleWrapper> =
wrapEspressoIdlingResource {
composeObservable { useCase(query, page) }
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Character>) {
if (isNext) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, params.key) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
//clear retry since last request succeeded
retry = null
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, params.key + 1)
}) {
retry = {
loadAfter(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Character>,
) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, 1) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, null, 2)
}) {
retry = {
loadInitial(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
Here is my wrapEspressoIdlingResource :
inline fun <T> wrapEspressoIdlingResource(task: () -> Observable<T>): Observable<T> = task()
.doOnSubscribe { EspressoIdlingResource.increment() } // App is busy until further notice
.doFinally { EspressoIdlingResource.decrement() } // Set app as idle.
But it does not wait until data delivered from network. When I Thread.Sleep before data delivered, Espresso test will be passed, so it is related to my Idling Resource setup.
I believe it could be related to Paging library, since this method works perfectly fine for Observable types when I use them in other samples without Paging library.
Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-Paging
What am I missing?
I needed to override the fetchDispatcher on the builder :
class BasePageKeyRepository<T>(
private val scheduler: BaseSchedulerProvider,
) : PageKeyRepository<T> {
#MainThread
override fun getItems(): Listing<T> {
val sourceFactory = getSourceFactory()
val rxPagedList = RxPagedListBuilder(sourceFactory, PAGE_SIZE)
.setFetchScheduler(scheduler.io()).buildObservable()
...
}
}
I'm trying to disconnect from WebSocket connection, but it's listener is still alive, I can see that websockets are still recreating by "OPEN/FAIL" messages using System.out messages.
On the one hand I've tried to release connection in finally block using client.dispatcher().executorService().shutdown() method, but it rejects other future calls (so it doesn't fit), on the other using client.connectionPool().evictAll() doesn't help either (even if I wait, because it may not exit immediately based on docs https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/)
There is WebSocketListener code:
class WebSocketConnectionListener(
private val updateConnectionValue: (internetConnection: Boolean) -> Unit,
private val okHttpClient: OkHttpClient,
private val urlAddress: String
) : WebSocketListener() {
companion object {
private const val NORMAL_CLOSURE_STATUS = 1000
}
private var isConnected = true
private var webSocketN: WebSocket? = null
init {
createWebSocket()
}
override fun onOpen(webSocket: WebSocket, response: Response) {
println("OPEN: ${response.code}")
isConnected = true
updateConnectionValue(true)
reconnect(webSocket)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
println("FAIL: $t")
if (!isConnected) {
updateConnectionValue(false)
}
isConnected = false
reconnect(webSocket)
}
private fun createWebSocket() {
val request = Request.Builder().url(urlAddress).build()
webSocketN = okHttpClient.newWebSocket(request, this)
}
private fun reconnect(webSocket: WebSocket) {
webSocket.close(NORMAL_CLOSURE_STATUS, null)
webSocketN?.close(NORMAL_CLOSURE_STATUS, "Connection closed")
webSocketN = null
Thread.sleep(3_000)
createWebSocket()
}
}
There is DataSource implementation code:
class InternetConnectionDataSourceImpl(
private val okHttpClient: OkHttpClient,
private val urlAddress: String
) : InternetConnectionDataSource {
private fun createWebSocketListener(
internetConnectionFlow: MutableStateFlow<Boolean>,
) = WebSocketConnectionListener(
updateConnectionValue = { internetConnectionFlow.value = it },
okHttpClient = okHttpClient,
urlAddress = urlAddress
)
override suspend fun checkConnection(): Flow<Boolean> =
withContext(Dispatchers.IO) {
val internetConnectionFlow = MutableStateFlow(true)
createWebSocketListener(internetConnectionFlow)
flow {
try {
internetConnectionFlow.collect {
emit(it)
}
} finally {
println("finally")
okHttpClient.connectionPool.evictAll()
}
}
}
}
As the result I'm getting this messages in logcat
Use WebSocket.close() for a graceful shutdown or cancel() for an immediate one.
https://square.github.io/okhttp/4.x/okhttp/okhttp3/-web-socket/
I'm newbie with RxJava2.
I have next code:
fun signIn(): Completable = getCredentials() // get saved token
.onErrorResumeNext { makeLockSignInRequest() } // if token not saved then get it
.flatMap { refreshToken(it) } // refresh token
.doOnSuccess { credentialsManager.saveCredentials(it) } // save updated token
.doFinally { lock?.onDestroy(context) }!!
.toCompletable()
private fun getCredentials() = Single.create(SingleOnSubscribe<Credentials> {
credentialsManager.getCredentials(object : BaseCallback<Credentials, CredentialsManagerException> {
override fun onSuccess(payload: Credentials?) = it.onSuccess(payload!!)
override fun onFailure(error: CredentialsManagerException?) = it.onError(error!!)
})
})
private fun makeLockSignInRequest() = Single.create(SingleOnSubscribe<Credentials> {
lock = Lock.newBuilder(auth0, object : AuthenticationCallback() {
override fun onAuthentication(credentials: Credentials?) = it.onSuccess(credentials!!)
override fun onCanceled() { }
override fun onError(error: LockException?) = it.onError(error!!)
})
.withScheme("demo")
.withScope("email openid offline_access")
.withAudience(ApiServiceProvider.DOMAIN + "/api/")
.closable(true)
.build(context)
context.startActivity(lock!!.newIntent(context))
})
private fun refreshToken(storedCredentials: Credentials) = Single.create(SingleOnSubscribe<Credentials> {
apiClient.renewAuth(storedCredentials.refreshToken!!)
.addParameter("scope", "openid email offline_access")
.start(object : BaseCallback<Credentials, AuthenticationException> {
override fun onSuccess(receivedCredentials: Credentials?) {
val newCredentials = Credentials(receivedCredentials!!.idToken, receivedCredentials.accessToken, receivedCredentials.type, storedCredentials.refreshToken, receivedCredentials.expiresAt, receivedCredentials.scope)
it.onSuccess(newCredentials)
}
override fun onFailure(error: AuthenticationException?) {
it.onError(Exception("Error refresh token: ${error!!.description!!}"))
}
})
})
This code gets saved token and refresh it.
Also if user just logged in it refresh token.
I want to add filter like follows:
fun signIn(): Completable = getCredentials()
.onErrorResumeNext { makeLockSignInRequest() }
.filter { OffsetDateTime.now(ZoneOffset.UTC).toEpochSecond() > it.expiresAt!!.time } // if token alive then do nothing
.flatMapSingle { refreshToken(it) }
.doOnSuccess { credentialsManager.saveCredentials(it) }
.doFinally { lock?.onDestroy(context) }!!
.toCompletable()
This code will fail with error: NoSuchElementException
So how can I filter token?
.filter changes your Single to Maybe. If there is no item in Maybe (because filter requirements are not met) after transforming it with flatMapSingle your code will return error with NoSuchElementException exception.
What I would do with it is:
fun signIn(): Completable = getCredentials()
.onErrorResumeNext { makeLockSignInRequest() }
.filter { OffsetDateTime.now(ZoneOffset.UTC).toEpochSecond() > it.expiresAt!!.time } // if token alive then do nothing
.flatMapCompletable { refreshToken(it).doAfterSuccess{credentialsManager.saveCredentials(it)}.toCompletable() }
.doFinally { lock?.onDestroy(context) }!!