I am building an offline first chat app and so I'm using Workmanager to handle POST requests to send chat messages.
I'm learning android development and so I wanted some help on architecturing the upload of chat messages
Current implementation
When ever a new chat message needs to be posted the client does the following
Saves the chat to SQLite using Room with a new UUID
Starts a workmanager unique work to POST this message
This way I can be sure the message is posted eventually when the client has internet
Is this ideal? There are a few issues I see.
I'm starting too many workers. Each message has a work request.
Chronology of messages posted to the server is lost.
A better implementation
A single unique worker to POST messages. Which will fetch all offline messages and post them in the right order
Still not ideal
The issues with these implementation are:
You have very little control on a work (not so easy) once it's started.
If a work fails we've set a backoff time. So when a new message is to be sent we need to replace the old worker with the new work request. This just seems nonoptimal.
We are mutating the worker instead of appending a new task to the queue.
We can't use the one worker per message implementation because we loose chronology and there are too many workers
This is sort of a distributed systems question.
We are starting workers who should work independ of the lifetime of the app
Workers should come back alive in a case they die (already managed by android-workmanager)
Workers should read from a queue of task to be executed (which is what I'm looking for)
There should be a persistance store that acts as a queue for the workers
There should be a service or a factory that invokes workers when ever needed (We don't have this in the current impl)
Questions
Is there a better way to post offline messages to the server when the client has internet? Like a service?
Is there a community build library that does this?
Can the current implementation be scaled to files?
I would need long running workers
Or could use this lib - android-upload-service
Yes you can use Service to effectively sync the messages, First create an Object which will extend from Live Data Class to get live updates for network updates like this,
object NetworkState : LiveData<Boolean>() {
private lateinit var application: Application
private lateinit var networkRequest: NetworkRequest
private lateinit var connectivityManager: ConnectivityManager
fun init(application: Application) {
this.application = application
connectivityManager = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
}
override fun onActive() {
super.onActive()
getDetails()
}
private fun getDetails() {
connectivityManager.registerNetworkCallback(networkRequest, object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
postValue(true)
}
override fun onLost(network: Network) {
super.onLost(network)
postValue(false)
}
override fun onUnavailable() {
super.onUnavailable()
postValue(false)
}
})
}
Then initialize this in your Application class like this,
#HiltAndroidApp
class BaseApplication : Application(), Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
override fun onCreate() {
super.onCreate()
NetworkState.init(this)
if (BuildConfig.DEBUG){
Timber.plant(Timber.DebugTree())
}
}
}
Now you can create a Service class which will extend from Lifecycle Service and observe the Network updated there. And when the Live Data will emit value true you can check if there are messages which needs to be synced online. But remember it will only work when the app is in foreground.
Related
I want to make an Android application that can record both incoming and outgoing calls in the background as a service in kotlin and at a particular time in the day, it sends all that recordings to a server by API. I had researched about it all I found is to use Device Policy Manager and Telephoney Manager but it is not much about it on the internet. So can you help me with any article, documentation, or tutorial?
There is no solution from Google as of now. Google has deprecated the feature of recording the calls in it's latest versions of Android OS. Earlier it was possible, I had tried various methods but I was only getting the silent audio when I had tried to record calls. When using Google's Phone application it only allows that application to use the microphone and other things it won't allow any other application to overpower and get that hardware access.
But there are actually two hacks to do that.
Build your own phone application like Truecaller and manage every call and other things from that application by doing this you can get access to managing calls on your device and you will also get the access to record the calls.
If your work is specific to any one mobile example like Samsung, OnePlus, etc. Then you can use any Truecaller or Google's Phone application which will store the recordings of the calls in file storage and then you can make a service to upload that call recording from that particular file location every night at 12 AM or something.
first create MyCallRecordReceiver class
class MyCallRecordReceiver(callRecord: CallRecord) : CallRecordReceiver(callRecord) {
override fun onIncomingCallReceived(context: Context, number: String?, start: Date) {
super.onIncomingCallReceived(context, number, start)
}
}
then in MainActivity
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = MainActivity::class.java.simpleName
}
private lateinit var callRecord: CallRecord
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
callRecord = CallRecord.Builder(this)
.setLogEnable(true)
.setRecordFileName("CallRecorderTestFile")
.setRecordDirName("CallRecorderTest")
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setShowSeed(true)
.build()
}
fun StartCallRecordClick(view: View) {
LogUtils.i(TAG, "StartCallRecordClick")
callRecord.startCallReceiver()
}
fun StopCallRecordClick(view: View) {
LogUtils.i(TAG, "StopCallRecordClick")
callRecord.stopCallReceiver()
}
}
In addition Add it as a dependency in your app's build.gradle file
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
and this
compile 'com.github.aykuttasil:CallRecorder:1.5.3'
I'm working on an Android app with a constant repeating background process.
From the moment the device starts it should load data off a webpage every minute. It uses XmlPullParser and a simple URL inputstream. It is but 10kb so it isn't that intensive. I believe this kind of task is called Deferred. The information loaded by the process has to be accessible to the Activity once that the user opens the app. The background process also needs to be abled to place a notification once the data shows certain results.
There seem to be multiple methods to achieve this in Android, eg. a JobScheduler, WorkManager or AlarmManager however everything I've tried so far seems to either stop once the activity closes or doesn't run at all. The timing, every minute, also seems to be an issue as for both a repeating job and worker the minimum interval is 15. This one minute doesn't have to be exact. I imagine instead of having a repeating process loading the data once it might be better to have a long running process sleeping for 1m in between loading the data.
I do not have access to the server the application is connecting to. so I can't do a FirebaseMessagingService.
What would be the best way to schedule such a background process?
How can the activity best exchange information with that process?
I'm open for all suggestions,
thank you for your time.
Easy with WorkManager, it's the most encouraged way for Scheduling Repeating background work in Android, see introduction.
As you say, the minimum repeating work request interval is restricted to 15 minutes, the only way to break it is to Repeatedly schedule the one-time work.
1. Setup Your Worker Class:
class ToastShower(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
withContext(Dispatchers.Main) { //ui related work must run in Main thread!!
Toast.makeText(applicationContext, "Hey, I'm Sam! This message will appear every 5 seconds.", Toast.LENGTH_SHORT).show()
}
return Result.success()
}
}
2. Setup Your Custom Application Class:
class WorkManagerApplication : Application() {
private val backgroundScope = CoroutineScope(Dispatchers.Default) //standard background thread
private val applicationContext = this
override fun onCreate() { //called when the app launches (same as Activity)
super.onCreate()
initWork()
}
private fun initWork() {
backgroundScope.launch { //all rnu in background thread
setupToastShowingWork(0) //no delay at first time
observeToastShowingWork() //observe work state changes, see below
}
}
private fun setupToastShowingWork(delayInSeconds: Long) { //must run in background thread
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) //when using WiFi
.build()
val oneTimeRequest = OneTimeWorkRequestBuilder<ToastShower>() //【for breaking 15 minutes limit we have to use one time request】
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS) //customizable delay (interval) time
.setConstraints(constraints)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork( //【must be unique!!】
ToastShower::class.java.simpleName, //work name, use class name for convenient
ExistingWorkPolicy.KEEP, //if new work comes in with same name, discard the new one
oneTimeRequest
)
}
private suspend fun observeToastShowingWork() {
withContext(Dispatchers.Main) { //must run in Main thread for using observeForever
WorkManager.getInstance(applicationContext).getWorkInfosForUniqueWorkLiveData(ToastShower::class.java.simpleName).observeForever {
if (it[0].state == WorkInfo.State.SUCCEEDED) { //when the work is done
backgroundScope.launch { //prevent from running in Main thread
setupToastShowingWork(5) //every 5 seconds
}
}
}
}
}
}
3. Setup AndroidManifest File:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.workmanagertest">
<application
android:name=".WorkManagerApplication" //【here, must!!!】
...
</application>
</manifest>
By setting up with above, the work (showing Toast in my example) will be executed (or more clearly, schedule and execute) every 5 seconds no matter the app is in foreground or background or killed by system. Only way to stop it is either uninstall or go inside the app's setting to force-close it.
Demo: https://youtu.be/7IsQQppKqFs
My Websocket client (OkHttp) doesn't close the connection after the app closed. It opens a new connection every time I open the app which makes the app suffering from multiple messages received on the old and the new Websocket connections from the broadcasting server.
Is that a normal behavior for the android client, as for what I have experienced with web-client, the session was closed properly after the tab killed?
I have been looking up the problem across the internet but no luck so far. I want to make sure whether it happened because of my bad code logic or just the buggy Websocket's implementation from the library?
Here is how I start a new websocket session in the main Activity
var request: Request = Request.Builder()
.url("ws://$serverIP:8080/example/sim/speed")
.build()
var webSocketListener: WebSocketListener = object : WebSocketListener() {
override fun callback(msg: Message) {
updateSpeed(msg.content)
}
override fun onClosing(webSocket: WebSocket?, code: Int, reason: String?) {
super.onClosing(webSocket, code, reason)
}
}
var webSocket = client!!.newWebSocket(request, webSocketListener)
After that updateSpeed() will update a text view on UIThread
The onClosed event was not triggered when the app closed but only when the close function called manually.
I'm sure that it opened a new socket every time because I can see new sessions created on the server with different ports.
What I want is to have the app closing its connection before it was closed.
Thank you
I also encountered this issue, that WebSocket was still opened after my app was killed.
To solve this issue I manually close socket and remove all idle connections:
fun clear() {
webSocket?.close(1001, "Android: User exited the game.")
webSocket = null
subs.clear()
subs.add(Completable.fromAction {
client.connectionPool.evictAll()
}.subscribeOn(Schedulers.io()).subscribe({}, {}))
}
And I basically call clear() inside activity/ies, that might be opened during app kill.
override fun onDestroy() {
super.onDestroy()
App.webSocketClient().clear()
}
Not an ideal solution, but works every time.
I believe I may not understand something about how gRPC Channels, Stubs, And Transports work. I have an Android app that creates a channel and a single blocking stub and injects it with dagger when the application is initialized. When I need to make a grpc call, I have a method in my client, that calls a method with that stub. After the app is idle a while, all of my calls return DEADLINE_EXCEEDED errors, though there are no calls showing up in the server logs.
#Singleton
#Provides
fun providesMyClient(app: Application): MyClient {
val channel = AndroidChannelBuilder
.forAddress("example.com", 443)
.overrideAuthority("example.com")
.context(app.applicationContext)
.build()
return MyClient(channel)
}
Where my client class has a function to return a request with a deadline:
class MyClient(channel: ManagedChannel) {
private val blockingStub: MyServiceGrpc.MyServiceBlockingStub = MyServiceGrpc.newBlockingStub(channel)
fun getStuff(): StuffResponse =
blockingStub
.withDeadlineAfter(7, TimeUnit.SECONDS)
.getStuff(stuffRequest())
}
fun getOtherStuff(): StuffResponse =
blockingStub
.withDeadlineAfter(7, TimeUnit.SECONDS)
.getOtherStuff(stuffRequest())
}
I make the calls to the server inside a LiveData class in My Repository, where the call looks like this: myClient.getStuff()
I am guessing that the channel looses its connection at some point, and then all of the subsequent stubs simply can't connect, but I don't see anywhere in the AndroidChannelBuilder documentation that talks about how to handle this (I believed it reconnected automatically). Is it possible that the channel I use to create my blocking stub gets stale, and I should be creating a new blocking stub each time I call getStuff()? Any help in understanding this would be greatly appreciated.
After researching a bit, I believe the issue was that the proxy on the server was closing the connection after a few minutes of idle time, and the client ManagedChannel didn't automatically detect that and connect again when that happened. When constructing the ManagedChannel, I added an idleTimeout to it, which will proactively kill the connection when it's idle, and reestablish it when it's needed again, and this seems to solve the problem. So the new channel construction looks like this:
#Singleton
#Provides
fun providesMyClient(app: Application): MyClient {
val channel = AndroidChannelBuilder
.forAddress("example.com", 443)
.overrideAuthority("example.com")
.context(app.applicationContext)
.idleTimeout(60, TimeUnit.SECONDS)
.build()
return MyClient(channel)
}
As explained in some answers:
On Android, Firebase automatically manages connection state to reduce bandwidth and battery usage. When a client has no active listeners, no pending write or onDisconnect operations, and is not explicitly disconnected by the goOffline method, Firebase closes the connection after 60 seconds of inactivity.
The problem is that after 60s, even after I go to an activity with a complete new reference, event listener, etc.. It still says it is disconnect, when in fact, it is not.
val connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected")
var connectListener : ValueEventListener? = null
fun checkConnection() {
connectListener = connectedRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val connected = snapshot.getValue(Boolean::class.java)!!
if (connected) {
Log.d("FRAG", "CONNECTED")
else{
Log.d("FRAG", "DISCONNECTED")
}
}
override
fun onCancelled(error: DatabaseError) {
System.err.println("Listener was cancelled")
}
})
}
override fun onDetach() {
super.onDetach()
if (connectListener != null){
connectedRef.removeEventListener(connectListener)
}
}
How can I make sure I maintain or create a new connection to Firebase? I call the checkConnection method every onAttach of a fragment and onStart of an activity.
If you have an active listener on any data that is read from the server, the connection should remain open unless you've explicitly called goOffline() in your code. Note that .info/connected itself does not require reading from the server, so does not keep the connection open.
It seems you're using the realtime database to build an presence system on an otherwise Firestore based app. In that case: Cloud Firestore uses a gRPC-based protocol to talk between client and server, while the Firebase Realtime Database uses web sockets. They're in no way compatible or even comparable. Keeping an active listener on data in Firestore does not keep a connection to RTDB open. That's why the example in the Firestore documentation also writes an actual data node to the realtime database.
Stream<Event> checkInternetConectivity() {
Stream<Event> connectivityCheck = _firebaseDatabase.reference().child('.info/connected').onValue;
Stream<Event> randomCheck = _firebaseDatabase.reference().child('connected').onValue;
return Rx.combineLatest2(connectivityCheck, randomCheck,(connectivityCheck, _) => connectivityCheck as Event);}
}
Firebase automatically disconnects from the realtime database in android after 60 seconds if there are no active listeners and listening to '.info/connected' isn't enough to keep the connection active. Creating another stream to listen to a random node in realtime database as a way around to this automatic disconnection.
This is my workaround to this problem in Dart/Flutter