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/
Related
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 new to Android Dagger-Hilt and I found it useful for my project. However, recently I want to use this concept to get my ServerConnection class to become Singleton across different view (fragment and activity). How can I achieve that?
I had tried to approach below but I can't get it Singleton as it will create 2 ServerConnection instance in my fragment/activity view. Where had I do wrong?
Current approach
AppModule.kt
#Singleton
#Provides
fun provideSocketConnection(tokenDao: TokenDao) : ServerConnection{
val token = runBlocking(Dispatchers.IO) { tokenDao.find() }
val tok = token!!.token
val refreshToken = token.refresh_token
return ServerConnection(URL)
}
ServerConnection.kt
class ServerConnection(url: String) {
enum class ConnectionStatus {
DISCONNECTED, CONNECTED
}
interface ServerListener {
fun onNewMessage(message: String?)
fun onStatusChange(status: ConnectionStatus?)
}
private var mWebSocket: WebSocket? = null
private val mClient: OkHttpClient
private val mServerUrl: String
private var mMessageHandler: Handler? = null
private var mStatusHandler: Handler? = null
private var mListener: ServerListener? = null
init {
mClient = OkHttpClient.Builder()
.readTimeout(3, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
mServerUrl = url
}
private inner class SocketListener : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
val m = mStatusHandler!!.obtainMessage(0, ConnectionStatus.CONNECTED)
mStatusHandler!!.sendMessage(m)
}
override fun onMessage(webSocket: WebSocket, text: String) {
val m = mMessageHandler!!.obtainMessage(0, text)
mMessageHandler!!.sendMessage(m)
}
override fun onClosed(
webSocket: WebSocket,
code: Int,
reason: String
) {
val m =
mStatusHandler!!.obtainMessage(0, ConnectionStatus.DISCONNECTED)
mStatusHandler!!.sendMessage(m)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
disconnect()
}
}
fun connect(listener: ServerListener?) {
val request = Request.Builder()
.url(mServerUrl)
.build()
mWebSocket = mClient.newWebSocket(request, SocketListener())
mListener = listener
mMessageHandler =
Handler(Handler.Callback { msg: Message ->
mListener?.onNewMessage(msg.obj as String)
true
})
mStatusHandler = Handler(Handler.Callback { msg: Message ->
mListener!!.onStatusChange(msg.obj as ConnectionStatus)
true
})
}
fun disconnect() {
mWebSocket?.cancel()
mListener = null
mMessageHandler?.removeCallbacksAndMessages(null)
mStatusHandler?.removeCallbacksAndMessages(null)
}
fun sendMessage(message: String?) {
mWebSocket!!.send(message!!)
}
}
View (Fragment/Activity)
#AndroidEntryPoint
class RoomFragment : Fragment(), ServerConnection.ServerListener {
#Inject lateinit var socketConnection: ServerConnection
}
You need to annotate your AppModule.kt class with #InstallIn(SinggltonComponent::class).
To know more about the hilt component, check this detail, here.
I am trying to call an API using Retrofit in an Android application using Kotlin. The API requires a
a header and body similar to the following sample input:
Sample Input
Header:
loginName: 65848614-6697-4cf7-a64a-e0b9374c4aee
Body:
clientId: DKOKTrykIQ987yQcLNehT8SWJRMyQLdP
secret: 6Jt1ENlDn9gxPu5f
The content type has to be passed as application/x-www-form-urlencoded.
Currently, I am using the following classes:
YodleeService.kt
interface YodleeService {
#Headers(
"loginName: de5559cc-5375-4aca-8224-990343774c08_ADMIN",
"Api-Version: 1.1",
"Content-Type: application/x-www-form-urlencoded"
)
#POST("auth/token")
fun generateLoginToken(
#Body postBody: PostBody
) : Call<LoginToken>
}
AccountRetriever.kt
class AccountRetriever {
private val service: YodleeService
companion object {
const val BASE_URL = "https://sandbox.api.yodlee.com:443/ysl/"
}
init {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(YodleeService::class.java)
}
fun login(callback: Callback<LoginToken>) {
val postBody = PostBody("TG86ljAk6lt28GYZlRTQWssaLmGpS6jV", "A2P9ZPEqB4uos1nv")
val call = service.generateLoginToken(postBody)
call.enqueue(callback)
}
}
PostBody
data class PostBody(
val clientId: String?,
val secret: String?
)
MainActivity
class MainActivity : AppCompatActivity() {
private val accountRetriever = AccountRetriever()
private val loginCallback = object : Callback<LoginToken> {
override fun onFailure(call: Call<LoginToken>, t: Throwable) {
Log.e("MainActivity", "Problem calling Yodlee API {${t.message}")
}
override fun onResponse(call: Call<LoginToken>?, response: Response<LoginToken>?) {
response?.isSuccessful.let {
Log.i("MainActivity", "errorBody - Content = ${response?.raw()}")
val loginToken = LoginToken(
response?.body()?.accessToken ?: "",
response?.body()?.expiresIn ?: "",
response?.body()?.issuedAt ?: "")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
accountsList.layoutManager = LinearLayoutManager(this)
if (isNetworkConnected()) {
accountRetriever.login(loginCallback)
} else {
AlertDialog.Builder(this).setTitle("No Internet Connection")
.setMessage("Please check your internet connection and try again")
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setIcon(android.R.drawable.ic_dialog_alert).show()
}
}
private fun isNetworkConnected(): Boolean {
val connectivityManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
return networkCapabilities != null &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
When I debug my application, I am getting a response of [size=213 text=\n {\n "errorCode": "Y303",\n …]. The documentation for the API says that this error code means the clientId or secret is missing.
When I dig through the debugger, I am seeing that the raw call reads as
Request {
method = POST, url = https: //sandbox.api.yodlee.com/ysl/auth/token,
tags = {
class retrofit2.Invocation =
com.example.budgettracker.api.YodleeService.generateLoginToken()[PostBody(
clientId = TG86ljAk6lt28GYZlRTQWssaLmGpS6jV, secret = A2P9ZPEqB4uos1nv)]
}
}
I cannot determine why the API is not seeing the POST body contents.
Any help would be greatly appreciated.
I am trying to write an Android Chess client using websockets. I chose the okhttp3 library. I can make a successful connection and can send data and receive. However, I am not sure how to return the data to LiveData for the ViewModel. I am somewhat familiar with Kotlin coroutines but I am not sure how I would get the data out of the listener.
I have tried trying to return from the function but as it is an overridden function I cannot return from it.
Here is the current WebSocketListener:
class EchoWebSocketListener : WebSocketListener() {
private val NORMAL_CLOSURE_STATUS = 1000
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
webSocket.send("Hello It is me")
webSocket.send("test 3!")
}
override fun onMessage(webSocket: WebSocket, text: String){
super.onMessage(webSocket, text)
outputData("Receiving $text")
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
super.onMessage(webSocket, bytes)
outputData("Receiving bytes : " + bytes.hex())
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
outputData("$code $reason")
}
private fun outputData(outputString: String) {
d("web socket", outputString)
}
}
And here is the setup code in the repository
fun startChat() {
httpClient = OkHttpClient()
val request = Request.Builder()
.url("ws://echo.websocket.org")
.build()
val listener = EchoWebSocketListener()
val webSocket = httpClient.newWebSocket(request, listener)
//webSocket.
httpClient.dispatcher.executorService.shutdown()
}
I would like to be able to run the repository with a Kotlin coroutine and return LiveData for the fragment to consume.
In your EchoWebSocketistener you could create a private MutableLiveData like so
class EchoWebSocketListener : WebSocketListener() {
private val _liveData = MutableLiveData<String>()
val liveData: LiveData<String> get() = _liveData
// Overridden methods
private fun outputData(string: String) {
_liveData.postValue(string)
}
}
Then you return the live data from the listener like so in a Coroutine
fun startChat(): LiveData<String> {
val listener = EchoWebSocketListener()
GlobalScope.launch(Dispatchers.IO) {
httpClient = OkHttpClient()
val request = Request.Builder()
.url("ws://echo.websocket.org")
.build()
val webSocket = httpClient.newWebSocket(request, listener)
//webSocket.
httpClient.dispatcher.executorService.shutdown()
}
return listener.liveData
}
I need to make an application where, while the user is authorized, it keeps the socket connection until it is logged out. For this purpose, a foreground service is created, which starts after the authorization of the user, and stops when it is logged out. It implements connection and reconnection on the socket.
All works well until you press the power button and turn off the charging. After this, the user stops receiving pongs from the server and the SocketTimeoutException is received on the OkHttp, and also stops receiving messages on the socket. On JavaWebsocket, The connection was closed because the other endpoint did not respond with a pong in time is received, after which you can successfully create a new socket connection, but it will repeat the same problem in the loop.
In the settings, the optimization of the battery for this application was disabled. What can I do to make a stable connection socket work in the background?
Implementation of activity:
class MainActivity : BaseFragmentPermissionActivity(), MainMvpView {
private var mIsSocketBound = false
private var mSocketBroadcastReceiver = SocketBroadcastReceiver(this)
private var mSocketConnection = SocketConnection(this)
private var mSocketService: SocketService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
doBindService()
}
private fun doBindService() {
bindService(Intent(this, SocketService::class.java), mSocketConnection, Context.BIND_AUTO_CREATE)
mIsSocketBound = true
}
override fun onStart() {
super.onStart()
...
mSocketService?.doStopForeground()
}
override fun onStop() {
mSocketService?.doStartForeground()
...
super.onStop()
}
override fun onDestroy() {
doUnbindService()
...
super.onDestroy()
}
private fun doUnbindService() {
if (mIsSocketBound) {
unbindService(mSocketConnection)
mIsSocketBound = false
mSocketService = null
}
}
class SocketConnection(mainActivity: MainActivity) : ServiceConnection {
private val mMainActivity: WeakReference<MainActivity> = WeakReference(mainActivity)
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val socketService = (service as SocketService.LocalBinder).getService()
mMainActivity.get()?.mSocketService = socketService
if (socketService.isForeground()) {
socketService.doStopForeground()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
mMainActivity.get()?.mSocketService = null
}
}
}
Implementation of service:
class SocketService : Service(), MvpErrorHandler {
private val mConnectingHandler = Handler()
private val mConnectingTask = ConnectingTask(this)
private var mIsRunningForeground = false
override fun onBind(intent: Intent?): IBinder {
startService(Intent(this, SocketService::class.java))
return mBinder
}
override fun onCreate() {
super.onCreate()
DaggerServiceComponent.builder()
.serviceModule(ServiceModule(this))
.applicationComponent(PatrolApplication.applicationComponent)
.build()
.inject(this)
startConnecting()
...
}
override fun onDestroy() {
...
stopConnecting()
super.onDestroy()
}
private fun startConnecting() {
if (!mIsConnecting) {
mIsConnecting = true
mConnectingHandler.post(mConnectingTask)
}
}
private fun stopConnecting() {
mConnectingHandler.removeCallbacks(mConnectingTask)
mIsConnecting = false
}
private fun openConnection() {
mCompositeDisposable.add(mDataManager.getSocketToken()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(false, this, {
stopConnecting()
mDataManager.openSocketConnection(it.token)
}, {
mConnectingHandler.postDelayed(mConnectingTask, RECONNECT_TIME.toLong())
return#subscribe ErrorHandlerUtil.handleGetSocketError(it, this)
}))
}
class ConnectingTask(socketService: SocketService) : Runnable {
private val mSocketService: WeakReference<SocketService> = WeakReference(socketService)
override fun run() {
mSocketService.get()?.openConnection()
}
}
}
Implementation of SocketHelper using JavaWebsocket:
class CustomApiSocketHelper #Inject constructor() : ApiSocketHelper {
private var mCustomSocketClient: WebSocketClient? = null
override fun openSocketConnection(token: String) {
mCustomSocketClient = CustomSocketClient(URI(CONNECTION_URL + token))
mCustomSocketClient?.connect()
}
override fun sendMessage(text: String) {
if (mCustomSocketClient?.isOpen == true) {
try {
mCustomSocketClient?.send(text)
} catch (t: Throwable) {
Log.e(TAG, Log.getStackTraceString(t))
Crashlytics.logException(t)
}
}
}
override fun closeSocketConnection() {
mCustomSocketClient?.close(CLOSE_REASON_OK)
}
class CustomSocketClient(uri: URI) : WebSocketClient(uri) {
init {
connectionLostTimeout = PING_TIMEOUT
}
override fun onOpen(handshakedata: ServerHandshake?) {
sendBroadcast(SocketActionType.OPEN.action)
}
override fun onMessage(message: String?) {
sendBroadcast(SocketActionType.MESSAGE.action, message)
}
override fun onClose(code: Int, reason: String?, remote: Boolean) {
if (code != CLOSE_REASON_OK) {
//call startConnecting() in service
sendBroadcast(SocketActionType.CLOSE.action)
}
}
override fun onError(ex: Exception?) {
sendBroadcast(SocketActionType.FAILURE.action)
}
private fun sendBroadcast(type: Int) {
val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
intent.putExtra(SOCKET_MESSAGE_TYPE, type)
LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
}
private fun sendBroadcast(type: Int, text: String?) {
val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
intent.putExtra(SOCKET_MESSAGE_TYPE, type)
intent.putExtra(SOCKET_MESSAGE, text)
LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
}
}
}
Implementation of SocketHelper using OkHttp:
class CustomApiSocketHelper #Inject constructor() : ApiSocketHelper {
private var mCustomSocketClient: WebSocket? = null
override fun openSocketConnection(token: String) {
val request = Request.Builder()
.url(CONNECTION_URL + token)
.build()
mCustomSocketClient = CustomApplication.applicationComponent.authorizedClient().newWebSocket(request, CustomSocketClient())
}
override fun sendMessage(text: String) {
mPatrolSocketClient?.send(text)
}
override fun closeSocketConnection() {
mCustomSocketClient?.close(CLOSE_REASON_OK, null)
}
class CustomSocketClient : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
sendBroadcast(SocketActionType.OPEN.action)
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
sendBroadcast(SocketActionType.MESSAGE.action, text)
}
override fun onClosed(webSocket: WebSocket?, code: Int, reason: String?) {
super.onClosed(webSocket, code, reason)
if (code != CLOSE_REASON_OK) {
sendBroadcast(SocketActionType.CLOSE.action)
}
}
override fun onFailure(webSocket: WebSocket?, t: Throwable?, response: Response?) {
super.onFailure(webSocket, t, response)
sendBroadcast(SocketActionType.FAILURE.action)
}
private fun sendBroadcast(type: Int) {
val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
intent.putExtra(SOCKET_MESSAGE_TYPE, type)
LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
}
private fun sendBroadcast(type: Int, text: String?) {
val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
intent.putExtra(SOCKET_MESSAGE_TYPE, type)
intent.putExtra(SOCKET_MESSAGE, text)
LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
}
}
}
...
#Provides
#Singleton
#Named(AUTHORIZED_CLIENT)
fun provideAuthorizedClient(builder: OkHttpClient.Builder, interceptor: Interceptor, authenticator: Authenticator): OkHttpClient = builder
.addInterceptor(interceptor)
.authenticator(authenticator)
.pingInterval(PING_TIMEOUT.toLong(), TimeUnit.SECONDS)
.build()
#Provides
#Singleton
fun provideOkHttpBuilder() = CustomApiHelper.getOkHttpBuilder()
fun getOkHttpBuilder(): OkHttpClient.Builder {
val builder = OkHttpClient.Builder()
builder.readTimeout(NETWORK_CALL_TIMEOUT, TimeUnit.SECONDS)
builder.writeTimeout(NETWORK_CALL_TIMEOUT, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
builder.addInterceptor(logger)
}
return builder
}
After some research and testing on different devices, it was found that for stable operation on the network, it is necessary that the device is charging or has a screen enabled. In the other case, neither PARTIAL_WAKE_LOCK nor the disabling of battery optimization in the settings itself can solve the problem.
The recommended way to solve this problem is to add this code to your activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
This prevents the screen from turning off and provides a stable socket connection. But we still have the situation that the user can press the power button. And, if at this moment the device is charging, everything will work as before, but otherwise, we will get the socket disconnect. To solve this problem, you need to periodically wake the device, in order to support the ping-pong process. This is not a recommended solution because it will lead to battery draining, and can not guarantee 100% performance, but if this moment is critical for you, then you can use this solution. You need to add this code, in a suitable place for you, in this example is used at the time of ping.
#Suppress("DEPRECATION")
override fun onWebsocketPing(conn: WebSocket?, f: Framedata?) {
if (mSocketWakeLock == null) {
mSocketWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK or PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG)
}
mSocketWakeLock?.takeIf { !it.isHeld }?.run { acquire(WAKE_TIMEOUT) }
super.onWebsocketPing(conn, f)
mSocketWakeLock?.takeIf { it.isHeld }?.run { release() }
}
Using this solution, on the test devices socket connection, with good Internet, stays stable for 2 hours or more. Without it, it is constantly disconnect.