0

I have an app that uses Service class to perform task in foreground.

This service also contains a Handler object to run same function multiple times. I want to change attributes in my activity_main.xml while functions are running in Service. For example when function calculates something in Service the result prints in TextView.

How it would be correct access activity_main's objects to retrieve and change their values and attributes?

Here is what I have:

MainActivity.kt:

class MainActivity : AppCompatActivity() {
    private var notificationManager: NotificationManager? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        buttonStart.setOnClickListener{
            buttonStart.isEnabled = false
            buttonStop.isEnabled = true
            IdListener.startService(this, "Foreground Service is running...")
        }
    }
}

IdListener.kt:

class IdListener : Service() {
    private val CHANNEL_ID = "ForegroundService Kotlin"
    private lateinit var mainHandler: Handler
    private lateinit var mRunnable: Runnable

    companion object {
        fun startService(context: Context, message: String) {
            val startIntent = Intent(context, IdListener::class.java)
            startIntent.putExtra("inputExtra", message)
            ContextCompat.startForegroundService(context, startIntent)
        }
        fun stopService(context: Context) {
            val stopIntent = Intent(context, IdListener::class.java)
            context.stopService(stopIntent)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

        mainHandler = Handler()
        mRunnable = Runnable { showRandomNumber(tm) }
        mainHandler.postDelayed(mRunnable, 1000)

        val input = intent?.getStringExtra("inputExtra")
        createNotificationChannel()
        val notificationIntent = Intent(this, MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            this,
            0, notificationIntent, 0
        )
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Foreground Service Kotlin Example")
            .setContentText(input)
            .setSmallIcon(R.drawable.ic_notofication)
            .setContentIntent(pendingIntent)
            .build()
        startForeground(1, notification)
        return START_NOT_STICKY
    }
    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        mainHandler.removeCallbacks(mRunnable)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(CHANNEL_ID, "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT)
            val manager = getSystemService(NotificationManager::class.java)
            manager!!.createNotificationChannel(serviceChannel)
        }
    }

    /// function in which I want elements from activity_main.xml to be changed
    fun showRandomNumber(manager: TelephonyManager){
        myTextView.text = "Working..." 
        mainHandler.postDelayed(mRunnable, 1000)
    }

}
Pavel Pereverzev
  • 459
  • 1
  • 6
  • 21
  • possible duplicate of [Communication between Activity and Service](https://stackoverflow.com/questions/20594936/communication-between-activity-and-service) – Mohammed Alaa Feb 10 '20 at 14:52
  • @MohammedAlaa as fas as I understood I need to `putExtra` some object to `IdListener`. Can you tell, which one that has all references to elements in activity_main.xml it should be, please? – Pavel Pereverzev Feb 10 '20 at 15:04
  • Proper encapsulation would not have the service needing to know about the details of the Activity. I would ignore most of the answers in that linked question, because they leak the Activity by binding a captured reference of the Activity. The proper solution is to register a broadcast receiver in the Activity that listens for new data and updates its own views. The Service can send broadcasts when it has new data. The data is passed using Intent extras. Your data is just Strings, so this is fine. – Tenfour04 Feb 10 '20 at 15:16
  • If you had more complicated data, you would maybe need to use binding, but be very careful about not capturing a reference to the activity in the bound class, possibly by using WeakReference. It would probably be much cleaner to create a singleton class (bound to the Application) that holds MutableLiveData of your data. Then the service can write new data, and the Activity can observe the data changes to react. – Tenfour04 Feb 10 '20 at 15:19
  • @Tenfour04 so for my needs like updating UI it would be enough to create a Broadcast reciever in MainActivity? Will the app run functions in Handler if I close it? – Pavel Pereverzev Feb 10 '20 at 15:46
  • It's been a long time since I've worked with services. Looks like LocalBroadcastReceiver is deprecated now! https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager.html So LiveData is probably the way to go. – Tenfour04 Feb 10 '20 at 15:50
  • @PavelPereverzev I think tenfour04 answer you question, also if you aren't comfortable with this way of handling changed date with LiveData you can use [ResultReceiver](https://developer.android.com/reference/android/os/ResultReceiver) it's for simple (activity to service) communication – Mohammed Alaa Feb 10 '20 at 20:11
  • @MohammedAlaa thanks! Will try both – Pavel Pereverzev Feb 10 '20 at 20:41

1 Answers1

0

Here's how I'd probably handle your case. I don't know exactly what you're doing, but I'm just having the text view show "Working..." when it starts the service until there's an ID available. I haven't tested this and haven't worked with services in a long time, so you might want other input.

object IdServiceData {
    val id = MutableLiveData<String>()
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        //...

        myTextView.text = "Working..." // should really use string resource here.
        IdServiceData.id.observe(this) {
            myTextView.text = it.value
        }
    }
}

When an Activity or Fragment observes a LiveData, they automatically stop observing when they are destroyed, so they are not leaked. So your Activity can be destroyed and recreated multiple times while the Service is running and it will keep getting the proper updates.

class IdListener : Service() {
    //...

    private fun broadcastNewId(id: String){
        mainHandler.post {
            IdServiceData.id.value = id
        }
    }
}

If you want better encapsulation, I suppose you could abstract out the MutableLiveData by creating a separate IdServiceDataProvider that has the MutableLiveData and is used by the service, and the IdServiceData would reference the data like this: val id: LiveData<String> = IdServiceDataProvider.id

Tenfour04
  • 83,111
  • 11
  • 94
  • 154