💉Inject LiveData into JobIntentService and How to🔫 Test It

— === MenU === —

📦1. MVVM Package …… → Menu

Gradle requirement:

    buildFeatures {
viewBinding true
}
//region activity and fragment
// Activity and Fragment
def activity_version = "1.2.1"
implementation "androidx.activity:activity-ktx:$activity_version"
def
fragment_version = "1.3.2"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
//endregion

MVVM

helper/LogHelper.kt

import android.util.Log

const val TAG = "MLOG"
fun
lgd(s:String) = Log.d(TAG, s)
fun lgi(s:String) = Log.i(TAG, s)
fun lge(s:String) = Log.e(TAG, s)
fun lgv(s:String) = Log.v(TAG, s)
fun lgw(s:String) = Log.w(TAG, s)

helper/MessageHelper.kt

import android.content.Context
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import android.widget.Toast.LENGTH_SHORT

fun msg(context: Context, s: String, len: Int) =
if (len > 0) Toast.makeText(context, s, LENGTH_LONG).show()
else Toast.makeText(context, s, LENGTH_SHORT).show()

🖌️ 2. UI Design …… → Menu

💼 3. JobIntentService …… → Menu

👨‍💻 Start Service: enqueueWork …… → Menu

fun enqueueWork(context: Context, work: Intent) {
enqueueWork(context, MyIntentService::class.java, JOB_ID, work)
}

⌛️ End Service …… → Menu

class MyIntentService: JobIntentService() {

init {
instance = this
}
companion object {
private lateinit var instance: MyIntentService
companion object {
private lateinit var instance: MyIntentService
private val JOB_ID = 4343443

fun enqueueWork(context: Context, work: Intent) {...}

fun stopService() {
lgd("MyIntentService: Service is stopping...")
instance.stopSelf()
}
}

🚪 4. Permission …… → Menu

📍 AndroidManifest.xml …… → Menu

<uses-permission android:name="android.permission.WAKE_LOCK" /><application
...
<activity android:name=".ui.MainActivity">
...
</activity>
<service android:name=".service.MyIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />

</application>

✒️ ui/MainActivity.kt …… → Menu

// check manifests for permissions
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.WAKE_LOCK
)

class MainActivity : AppCompatActivity() {

// app permission
private val reqMultiplePermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.entries.forEach {
lgd("mainAct: Permission: ${it.key} = ${it.value}")
if (!it.value) {
// toast
msg
(this, "Permission: ${it.key} denied!", 1)
finish()
}
}
}

// =============== Variables
// view binding
private lateinit var binding: ActivityMainBinding
// view model
val viewModel: MainViewModel by viewModels()

// =============== END of Variables

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

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

// check app permissions
reqMultiplePermissions.launch(REQUIRED_PERMISSIONS)
}

companion object {
const val USER_INPUT = "USER_INPUT"
}
}

⌚5. Observables & Hilt …… → Menu

🔆 app/ServiceApp.kt …… → Menu

@HiltAndroidApp
class ServiceApp: Application()

🗡 di/LiveDataModule.kt …… → Menu

@Module
@InstallIn(SingletonComponent::class)
object LiveDataModule {

@Provides
@Singleton
fun provideServiceStatus():
MutableLiveData<Boolean> = MutableLiveData<Boolean>()

@Provides
@Singleton
fun provideUserInput():
MutableLiveData<String> = MutableLiveData<String>()

}

♈️ ui/MainViewModel.kt …… → Menu

@HiltViewModel
class MainViewModel @Inject constructor(
val isRunning: MutableLiveData<Boolean>,
private val userInput: MutableLiveData<String>

): ViewModel() {

init {
isRunning.value = false
userInput.value = ""
}

fun enableService() {
isRunning.postValue(true)
}

fun updateUserInput(inputText: String) {
userInput.postValue(inputText)
}

}

🔧 service/MyIntentService.kt …… → Menu

@AndroidEntryPoint
class MyIntentService: JobIntentService() {

@Inject
lateinit var isRunning: MutableLiveData<Boolean>

@Inject
lateinit var userInput: MutableLiveData<String>

init {
instance = this
}

override fun onHandleWork(intent: Intent) {
lgd("onHandleWork")
try {
lgd("MyIntentService: Service is running...")
if (isRunning.value!!) {

// check Intent Extra
val extraInput = intent.getStringExtra(USER_INPUT)
lgd("Intent Extra: $extraInput")

var input = "Empty"
if
(userInput.value != "")
input = userInput.value.toString()

lgd("receive text from LiveData: $input")

for (i in 0..9) {
lgd("Input: $input - $i")
if (isRunning.value == false)
return
SystemClock.sleep(1000)
}
stopService()
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
}
...

🔨 Setup observers in ui/MainActivity.kt …… → Menu

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...

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

binding.btStart.setOnClickListener {
viewModel
.enableService()

// update user input
val inputText = binding.etMessage.text
if (!inputText.isEmpty() || !inputText.isBlank())
viewModel.updateUserInput(inputText.toString())
lgd("input text: $inputText")

val mIntent = Intent(this, MyIntentService::class.java)
mIntent.putExtra(USER_INPUT, inputText)

// start service
MyIntentService.enqueueWork(this, mIntent)
}

binding
.btStop.setOnClickListener {
MyIntentService.stopService()

}

// observer
viewModel.isRunning.observe(this, {
if
(it)
binding.tvService.text = "Service is Start..."
else
binding
.tvService.text = "Service is Stop!"
}
)
}

companion object {...}
}

🏃🏻6. Running Result …… → Menu

--------- beginning of system
2021-04-24 14:39:27.113 6199-7154 D/MLOG: onHandleWork
2021-04-24 14:39:27.113 6199-7154 D/MLOG: MyIntentService: Service is running...
2021-04-24 14:39:27.123 6199-7154 D/MLOG: Intent Extra: null
2021-04-24 14:39:27.123 6199-7154 D/MLOG: receive text from LiveData: Empty
2021-04-24 14:39:27.123 6199-7154 D/MLOG: Input: Empty - 0
2021-04-24 14:39:28.164 6199-7154 D/MLOG: Input: Empty - 1
2021-04-24 14:39:29.205 6199-7154 D/MLOG: Input: Empty - 2
2021-04-24 14:39:30.246 6199-7154 D/MLOG: Input: Empty - 3
2021-04-24 14:39:31.273 6199-7154 D/MLOG: Input: Empty - 4
2021-04-24 14:39:32.318 6199-7154 D/MLOG: Input: Empty - 5
2021-04-24 14:39:33.360 6199-7154 D/MLOG: Input: Empty - 6
2021-04-24 14:39:34.375 6199-7154 D/MLOG: Input: Empty - 7
2021-04-24 14:39:35.380 6199-7154 D/MLOG: Input: Empty - 8
2021-04-24 14:39:36.384 6199-7154 D/MLOG: Input: Empty - 9
2021-04-24 14:39:37.387 6199-7154 D/MLOG: MyIntentService: Service is stopping...
2021-04-24 14:44:55.110 6199-6199 D/MLOG: input text: Welcome
2021-04-24 14:44:55.126 6199-7154 D/MLOG: onHandleWork
2021-04-24 14:44:55.126 6199-7154 D/MLOG: MyIntentService: Service is running...
2021-04-24 14:44:55.127 6199-7154 D/MLOG: Intent Extra: null
2021-04-24 14:44:55.127 6199-7154 D/MLOG: receive text from LiveData: Welcome
2021-04-24 14:44:55.127 6199-7154 D/MLOG: Input: Welcome - 0
2021-04-24 14:44:56.168 6199-7154 D/MLOG: Input: Welcome - 1
2021-04-24 14:44:57.211 6199-7154 D/MLOG: Input: Welcome - 2
2021-04-24 14:44:58.251 6199-7154 D/MLOG: Input: Welcome - 3
2021-04-24 14:44:59.293 6199-7154 D/MLOG: Input: Welcome - 4
2021-04-24 14:45:00.334 6199-7154 D/MLOG: Input: Welcome - 5
2021-04-24 14:45:01.377 6199-7154 D/MLOG: Input: Welcome - 6
2021-04-24 14:45:02.418 6199-7154 D/MLOG: Input: Welcome - 7
2021-04-24 14:45:03.460 6199-7154 D/MLOG: Input: Welcome - 8
2021-04-24 14:45:04.501 6199-7154 D/MLOG: Input: Welcome - 9
2021-04-24 14:45:05.542 6199-7154 D/MLOG: MyIntentService: Service is stopping...

☕ 7. Test Service with Espresso …… → Menu

service/MyIntentServiceTest.kt

@ExperimentalCoroutinesApi
@LargeTest
@HiltAndroidTest
class MyIntentServiceTest {

@get:Rule(order = 1)
var hiltRule = HiltAndroidRule(this)

@get:Rule(order = 2)
var activityRule = ActivityScenarioRule(MainActivity::class.java)


@Before
fun setup() {
hiltRule.inject()
}

🍬 helper/Constants.kt …… → Menu

const val START = "Service is Start..."
const val
STOP = "Service is Stop!"

Stop Test Case:

@Test
fun test_stop_service_espresso() {
lgd("=====> Stop Service Test")

// start activity
val scenario = activityRule.getScenario()

onView(withId(R.id.bt_start)).perform(click())
lgd("=====> Start Button Clicked")

onView(withId(R.id.bt_stop)).perform(click())
lgd("=====> Stop Button Clicked")

val serviceMsg = onView(withId(R.id.tv_service))
serviceMsg.check(ViewAssertions.matches(
ViewMatchers.withText(STOP)))
}
2021-04-27 07:57:31.223 20493-20703 D/MLOG: MyIntentService: Service is running...
2021-04-27 07:57:31.230 20493-20703 D/MLOG: Intent Extra: null
2021-04-27 07:57:31.230 20493-20703 D/MLOG: receive text from LiveData: Empty
2021-04-27 07:57:31.230 20493-20703 D/MLOG: Input: Empty - 0
2021-04-27 07:57:32.278 20493-20703 D/MLOG: Input: Empty - 1
2021-04-27 07:57:33.313 20493-20703 D/MLOG: Input: Empty - 2
2021-04-27 07:57:34.345 20493-20703 D/MLOG: Input: Empty - 3
2021-04-27 07:57:35.356 20493-20703 D/MLOG: Input: Empty - 4
2021-04-27 07:57:36.393 20493-20703 D/MLOG: Input: Empty - 5
2021-04-27 07:57:37.434 20493-20703 D/MLOG: Input: Empty - 6
2021-04-27 07:57:38.473 20493-20703 D/MLOG: Input: Empty - 7
2021-04-27 07:57:39.475 20493-20703 D/MLOG: Input: Empty - 8
2021-04-27 07:57:40.521 20493-20703 D/MLOG: Input: Empty - 9
2021-04-27 07:57:41.558 20493-20703 D/MLOG: MyIntentService: Service is stopping...
2021-04-27 07:57:41.792 20493-20533 D/MLOG: =====> Start Button Clicked
2021-04-27 07:57:41.850 20493-20493 D/MLOG: MainAct: stop button clicked!
2021-04-27 07:57:41.850 20493-20493 D/MLOG: MyIntentService: Service is stopping...
2021-04-27 07:57:42.089 20493-20533 D/MLOG: =====> Stop Button Clicked

🚍 8. UiAutomator — Stop Test …… → Menu

📌 gradle.module …… → Menu

//UiAutomator
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'

MyIntentJobServiceTest

@get:Rule(order = 1)
var hiltRule = HiltAndroidRule(this)
private var mDevice: UiDevice? = null

Setup the App

@Before
fun setup() {
hiltRule.inject()

// Initialize UiDevice instance
mDevice = UiDevice.getInstance(getInstrumentation())
mDevice!!.pressMenu()
val launcherPackage = mDevice!!.launcherPackageName
Truth.assertThat(launcherPackage).isNotNull()

mDevice!!.wait(
Until.hasObject(By.pkg(launcherPackage).depth(0)),
LAUNCH_TIMEOUT
)

// launch app
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = context.packageManager.getLaunchIntentForPackage(
APPLICATION_ID)?.apply {
// Clear out any previous instances
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
context.startActivity(intent)

// Wait for the app to appear
mDevice!!.wait(
Until.hasObject(By.pkg(APPLICATION_ID).depth(0)),
LAUNCH_TIMEOUT
)
}
companion object {
const val LAUNCH_TIMEOUT = 5000L
}

Stop Service Test Case

@Test
fun test_stop_service_uiautomator() {
}

🔍 Find Object by ID …… → Menu

val $var$ = mDevice!!.findObject(
res("${APPLICATION_ID}:id/$obj$"))
// buttons
val startBt = mDevice!!.findObject(
res("${APPLICATION_ID}:id/bt_start"))
val stopBt = mDevice!!.findObject(
res("${APPLICATION_ID}:id/bt_stop"))
lgd("start bt: ${startBt.resourceName}")
startBt.click()

Thread.sleep(1000L)

lgd("stop bt: ${stopBt.resourceName}")
stopBt.click()

// service message
val serviceMsg = mDevice!!.findObject(
res("${APPLICATION_ID}:id/tv_service"))
val statusStr = serviceMsg.text
Truth.assertThat(statusStr).isEqualTo(STOP)

🚁 9. UiAutomator — UserInput Test + Logcat …… → Menu

👁‍🗨 LogOutput.kt …… → Menu

/**
* Editor: Homan Huang
* Date: 04/27/2021
*/
/**
* Get Logcat output by getOutput("logcat *:S TAG -d")
*/
fun getOutput(command: String): String? {
val proc = Runtime.getRuntime().exec(command)

try {
val stdInput = BufferedReader(
InputStreamReader(
proc.inputStream
)
)
val output = StringBuilder()
var line: String? = ""
//var counter = 0
while (stdInput.readLine().also { line = it } != null) {
//counter += 1
//lgd("line #$counter = $line")
output.append(line+"\n")
}
stdInput.close()
return output.toString()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}

/**
* clear logcat buffer
*/
fun clearLog() {
Runtime.getRuntime().exec("logcat -c")
}

User Input Service Test

/**
* Test: Input the message;
* start the service;
* and check logcat
*/
@Test
fun message_input_service_uiautomator() {
// buttons
val msgInput = mDevice!!.findObject(
res("${APPLICATION_ID}:id/et_message"))
val startBt = mDevice!!.findObject(
res("${APPLICATION_ID}:id/bt_start"))
val stopBt = mDevice!!.findObject(
res("${APPLICATION_ID}:id/bt_stop"))

// clear Logcat buffer
clearLog()

// input
val toServiceStr = "This is a test."
msgInput.text = toServiceStr
Thread.sleep(1000)

lgd("start bt: ${startBt.resourceName}")
startBt.click()

Thread.sleep(1000)

lgd("stop bt: ${stopBt.resourceName}")
stopBt.click()

val param = "logcat *:S MLOG -d"
lgd("param: $param")
val mLog = getOutput(param)

Thread.sleep(500)

lgd("mlog: $mLog")

Truth.assertThat(mLog?.contains("Input: $toServiceStr"))
.isTrue()
}
mlog: --------- beginning of main
04-27 14:06:29.582 31368 31368 D MLOG : mainAct: Permission: android.permission.WAKE_LOCK = true
04-27 14:06:31.459 31368 31400 D MLOG : start bt: com.homan.huang.servicedemo:id/bt_start
04-27 14:06:31.504 31368 31368 D MLOG : MainAct: start button clicked!
04-27 14:06:31.504 31368 31368 D MLOG : input text: This is a test.
04-27 14:06:31.527 31368 31419 D MLOG : onHandleWork
04-27 14:06:31.527 31368 31419 D MLOG : MyIntentService: Service is running...
04-27 14:06:31.528 31368 31419 D MLOG : Intent Extra: null
04-27 14:06:31.528 31368 31419 D MLOG : receive text from LiveData: This is a test.
04-27 14:06:31.528 31368 31419 D MLOG : Input: This is a test. - 0
04-27 14:06:32.495 31368 31400 D MLOG : stop bt: com.homan.huang.servicedemo:id/bt_stop
04-27 14:06:32.519 31368 31400 D MLOG : param: logcat *:S MLOG -d
04-27 14:06:32.525 31368 31368 D MLOG : MainAct: stop button clicked!
04-27 14:06:32.525 31368 31368 D MLOG : MyIntentService: Service is stopping...

Clap, clap… …… → Menu
😊 Hope You Have A Good Testing DAY!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store