I cannot seem to create an infinite loop that runs periodically for a background service and is reliable.
The app uses a Service to post a GPS location every 5 seconds using a FusedLocationClient LocationCallback.
The functions loop as expected when running on the main thread, however the functions stop looping shortly after starting a different app. This is the case for a new thread, as well as a background service, and even a new thread created by a background service, it consistently stops looping shortly after starting a different app. Shortly thereafter onDestroy() is called.
The only way I have been able to successfully continue looping a function in a Service while the user is in a different app is by having a while(true) loop. However, when I implement this method, I never get a call back from the FusedLocationClient. I cannot figure out why or how to get around this problem.
I have already reviewed the Android Guides and API documentation for
Background processing, Background Service, Handler, Looper, Thread.
As well as the StackExchange questions:how to run infinite loop in background thread and restart it, How to run an infinite loop in Android without freezing the UI.
My question is how do I maintain a continuous loop in a background service that does not interfere with the UI AND does not interfere with the FusedLocationCallback
Below is a snippet of my code.
And yes, I declared everything correctly in the manifest.
class MyService: Service(){
private lateinit var locationRequest: LocationRequest
private lateinit var locationCallback: LocationCallback
private lateinit var looper: Looper
private lateinit var context: Context
data class postGPS(...)
val runnable: Runnable = object : Runnable {
override fun run() {
getLocation()
}
}
override fun onStartCommand(...): Int {
context = this
val thread = BackgroundThread()
thread.start()
return START_STICKY
}
inner class BackgroundThread: Thread(){
override fun run() {
Looper.prepare()
looper = Looper.myLooper()!!
handler = Handler(looper)
getLocation()
Looper.loop()
}
}
#SuppressLint("MissingPermission")
fun getLocation()
{
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context!!)
locationRequest = LocationRequest()
locationRequest.interval = 1000
locationRequest.fastestInterval = 1000
locationRequest.smallestDisplacement = 10f
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
locationResult ?: return
if (locationResult.locations.isNotEmpty()) {
PostGPS(postGPS(locationResult.lastLocation)
}
}
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
looper
)
}
private fun PostGPS(gps: postGPS){
val queue = Volley.newRequestQueue(context)
val url = "https://www.URL.com/api"
val stringReq: StringRequest =
object : StringRequest(
Method.POST, url,
Response.Listener { response ->
Toast.makeText(context, response, Toast.LENGTH_SHORT).show()
handler.postDelayed(runnable,5000) //Loop for posting GPS location
},
Response.ErrorListener { error ->
Toast.makeText(context, error.message.toString(),Toast.LENGTH_SHORT).show()
}
) {
override fun getBody(): ByteArray {
return gps.toByteArray(Charset.defaultCharset())
}
}
queue.add(stringReq)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
}
}
Related
I want to handle something in my ViewModel whenever the current location retrieved. But it didn't work at the first time I start the app and approve the permission. Only be able to see some logs after I close and start the app again.
init {
viewModelScope.launch {
locationRepository.location.collect {
Log.d(TAG, it.toString())
My repository to connect the location data source as you can see
class LocationRepositoryImpl #Inject constructor(
private val dataSource: LocationDataSource,
#ApplicationScope private val externalScope: CoroutineScope
) : LocationRepository {
override val location: Flow<MapLocation> = dataSource.locationSource
.shareIn(
scope = externalScope,
started = WhileSubscribed()
And the final is LocationDataSource where I put the logic to get the current location.
class LocationDataSource #Inject constructor(
private val client: FusedLocationProviderClient
) {
val locationSource: Flow<MapLocation> = callbackFlow {
val request = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(4)
fastestInterval = TimeUnit.SECONDS.toMillis(4)
priority = Priority.PRIORITY_HIGH_ACCURACY
}
val callBack = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
super.onLocationResult(result)
result.lastLocation?.let {
trySend(it.asModel())
}
}
}
//Subscribe to location changes.
client.requestLocationUpdates(request, callBack, Looper.getMainLooper())
awaitClose { client.removeLocationUpdates(callBack) }
The cause is I still did use the onPermissionResult() in my fragment, so after switch to the new requestPermissionLauncher = registerForActivityResult, and call onForegroundPermissionApproved() instead of in init of ViewModel, after approve the location permission. Everything work properly.
I'm using FusedLocationProvider in my app and I noticed that when my app is in the background and I start some other app that contains Google Map my original app starts receiving location updates extremely fast (like 1 update per second) despite setting up the fastest interval.
I know that I should unregister when going to background etc but this is not the case here.
Any ideas why this might happen or where I can report it to Google?
This is the activity I start it from (I've removed couple of permissions check just for the visibility)
The full repo can be found here
class MainActivity : AppCompatActivity() {
private val locationController by lazy { LocationController.getInstance(applicationContext) }
lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button)
button.setOnClickListener {
if (locationController.isStarted) {
locationController.stop()
button.text = "START LOCATION UPDATES"
} else {
locationController.start()
button.text = "STOP LOCATION UPDATED"
}
}
}
And the LocationController looks like this:
class LocationController(context: Context) {
companion object {
#Volatile private var INSTANCE: LocationController? = null
fun getInstance(context: Context): LocationController {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: LocationController(context).also { INSTANCE = it }
}
}
}
private val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
private val locationRequest by lazy {
LocationRequest.create()
.setInterval(INTERVAL_MILLIS)
.setFastestInterval(FASTEST_INTERVAL_MILLIS)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
}
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
Log.d("boom", "onLocationResult! ${locationResult.lastLocation}")
}
override fun onLocationAvailability(locationAvailability: LocationAvailability) {
super.onLocationAvailability(locationAvailability)
}
}
var isStarted: Boolean = false
#SuppressLint("MissingPermission")
fun start() {
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
.addOnSuccessListener {
Log.d("boom", "requestLocationUpdates success!")
}
isStarted = true
}
fun stop() {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
.addOnSuccessListener {
Log.d("boom", "removeLocationUpdates success!")
}
isStarted = false
}
The constant values I experience it with are:
const val INTERVAL_MILLIS = 30_000L
const val FASTEST_INTERVAL_MILLIS = 10_000L
I am trying to make a service performs and action at intervals, with the help of this article I was able to set up a service and set and interval of 1000miliseconds to log to my console, but I noticed that the service only runs once. Here is a snippet of my code:
class MessageService : Service() {
private var serviceLooper: Looper? = null
private var serviceHandler: ServiceHandler? = null
override fun onCreate() {
val context:Context = this
HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
serviceHandler = ServiceHandler(context, looper)
}
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
serviceHandler?.obtainMessage()?.also { msg ->
msg.arg1 = startId
serviceHandler?.sendMessage(msg)
}
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? { return null}
override fun onDestroy() {}
private inner class ServiceHandler(context: Context, looper: Looper) : Handler(looper) {
val baseContext = context
override fun handleMessage(msg: Message) {
val runnable = Runnable {
Log.i("thread", "service has been called")
}
this.postDelayed(runnable, 1000)
}
}
}
please what am I doing wrong?
The main problem is that you're creating a NEW Handler using val handler = Handler() after the first correct "handleMessage()" call, so each postDelayed() will post the runnable in an "unmanaged" Handler (I call "unmanaged" because that just-created-one hasn't overridden the "handleMessage(..)" method).
You need to postDelayed() to serviceHandler and not to a new Handler. To fix you can use this.postDelayed(...) because you're inside ServiceHandler and you want to post the Runnable in the same Handler.
This is the code with problems :
object MainThreadPoster : Handler(Looper.getMainLooper()) {
fun postRunnableAtFixRate(runnable: Runnable, token: Any, delay: Long, period: Long) {
postAtTime(object : Runnable {
override fun run() {
runnable.run()
}
}, token, SystemClock.uptimeMillis() + delay)
}
}
The MainThreadPoster is initialized with mainLooper, so the runnable function (in the postRunnableAtFixRate method) is expected to be executed in the main thread, but the problem is that the runnable function may be executed in a HandlerThread sometime.
This is the expected stack trace
This is the stack trace with problem
Do not invoke Message.recycle() in your code. In Android 4.4, if you invoke Message.recycle() multi time, the message will occur in the message pool multi times, and the message may exist in multi message queue at the same time。
this is the poc:
class MainActivity : AppCompatActivity() {
private val mMainHandler = Handler(Looper.getMainLooper())
private lateinit var mSubHandler: Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_test.setOnClickListener {
start()
}
val handlerThread = HandlerThread("sub thread")
handlerThread.start()
mSubHandler = Handler(handlerThread.looper)
}
private fun start(){
val message = Message()
message.recycle()
message.recycle()
mMainHandler.postDelayed({
if(Looper.myLooper() != Looper.getMainLooper()){
throw Exception("should run in main thread")
}
start()
}, 100)
mSubHandler.sendEmptyMessage(1)
}
}
In My project sometimes the created thread does not start as fast as it should be, This happens on a minimal occasions but mostly will happen on slow/older phones.
I my Thread like..
class DBThread(threadName: String) : HandlerThread(threadName) {
private var mWorkerHandler: Handler? = null
override fun onLooperPrepared() {
super.onLooperPrepared()
mWorkerHandler = Handler(looper)
}
fun createTask(task: Runnable) {
mWorkerHandler?.post(task)
}
}
and when i use it and call on activity..
//this will handle db queries on background and not on main ui thread
var mDbThread: DBThread = DBThread("dbThread")
//use this to interact to main ui thread from different thread
val mUiHandler = Handler()
var mDb: LocalDatabase? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDbThread.start()
mDb = LocalDatabase.getInstance(this)
fetchAndSetList()
}
override fun onDestroy() {
super.onDestroy()
LocalDatabase.destroyInstance()
mDbThread.quitSafely()
}
private fun fetchAndSetList(){
mDbThread.createTask(Runnable {
val list = getList()
mUiHandler.post {
// this sometimes does not trigger
setList(list)
}
})
}
the function setList does not trigger on sometimes.
And so i did something like this.
fun createTask(task: Runnable) {
if(mWorkerHandler == null ){
createTask(task)
return
}
mWorkerHandler?.post(task)
}
the modified action seems to work however I'm not quite sure if this is a SAFE way to do this. Thank you in advance.
I think the reason why mWorkerhandler is null is because Thread.start will create the new VMThread and start the looper in the VMThread. The whole flow is asynchronous so when onLooperPrepared actually is called, it's too late because "fetchAndSetList" is already trying to use mWorkerHandler
The solution is create the handler outside of the HandlerThread:
Handler workerHandler;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDbThread.start()
workerHandler = new Handler(mDbThread.getLooper());
mDb = LocalDatabase.getInstance(this)
fetchAndSetList()
}
private fun fetchAndSetList(){
workerHandler.post(Runnable {
val list = getList()
mUiHandler.post {
// this sometimes does not trigger
setList(list)
}
})
}