0. Introduction Service
Service doesn't have to be used for a long time. Isn't it a long servant? We can use JobIntentService -- temporary servant. It's good to live and die with your App! However, it's easy to start and there's no clue to shut down. Because the service runs in the background, it has nothing to do with the UI. If we use MVVM, we can plug LiveData. Start through the equation, and the system will pop up "LiveData has not initialized". If you use the constructor of service, the system will say that it does not accept parameters. Spare your head, right?
It doesn't matter. We can use plug-in. What I propose is dagger hilt to inject the system.
📦 1. MVVM package
🌮 Gradle - database selection:
- View Binding:
buildFeatures { viewBinding true }
- Dagger Hilt ——Please install by yourself.
- ViewModel:
//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
Just mention it here. In fact, you can copy what I wrote before. Gradle takes up too much space, so omit one or two.
...
🔰 MVVM - file arrangement
🖐🏻 Helper - Helper
- 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 graphic design
et_message: enter the data into the Service.
tv_service: service response.
💼 3. JobIntentService
JobIntentService is an improved version of IntentService.
🐔 Start service
This service is started with an equation - enqueueWork
fun enqueueWork(context: Context, work: Intent) { enqueueWork(context, MyIntentService::class.java, JOB_ID, work) }
This enqueueWork has four parameters:
- Context
- Service class
- Job ID
- Intent
⌛ Shut down the service
Close with instance.
class MyIntentService: JobIntentService() { init { instance = this } 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() } } }
You see, shut yourself up.
🚪 4. Permission
📍 AndroidManifest.xml
<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
// 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 observation and injection
👁🗨 Observation point
I need to provide two observation points:
- isRunning: service status.
- userInput: the content entered by the customer. There are two ways: IntentExtra and LiveData. I'll test that guaranteed.
🔆 app/ServiceApp.kt provides Hilt application
@HiltAndroidApp class ServiceApp: Application()
...
🗡 di/LiveDataModule.kt
@Module @InstallIn(SingletonComponent::class) object LiveDataModule { @Provides @Singleton fun provideServiceStatus(): MutableLiveData<Boolean> = MutableLiveData<Boolean>() @Provides @Singleton fun provideUserInput(): MutableLiveData<String> = MutableLiveData<String>() }
Easy, just serve these two.
...
♈️ ui/MainViewModel.kt
@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) } }
The main function is to update the screen.
...
🔧 service/MyIntentService.kt
@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() } } ...
...
🔨 ui/MainActivity.kt
@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 {...} }
Press the key to open the service and close the service.
Another is to observe the service status.
🏃🏻 6. Operation results
In good condition. Look at Logcat:
--------- 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...
Join Welcome and see if the Service receives it.
Logcat:
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...
Intextera receive failed.
LiveData succeeded.
☕ 7. Espresso test
➕ Add test
Right click "class MyIntentService" and Alt+Insert.
Select Junit4:
Android test folder:
🔭 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 -- constant
const val START = "Service is Start..." const val STOP = "Service is Stop!"
...
❌ Stop Test Case -- stop the test
@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))) }
It seems normal. Let's take a look at Logcat:
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
Terrible. Espresso didn't come out to work until the Service stopped. Therefore, we need another tool to test - UiAutomator.
🚍 8. UiAutomator
📌 gradle.module
//UiAutomator androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
Sync.
...
📢 MyIntentJobServiceTest
@get:Rule(order = 1) var hiltRule = HiltAndroidRule(this)
This reservation.
private var mDevice: UiDevice? = null
Start:
@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 }
Add Test:
@Test fun test_stop_service_uiautomator() { }
🔍 Find object with ID
UiAutomator looks for things the same as Android:
val $var$ = mDevice!!.findObject( res("${APPLICATION_ID}:id/$obj$"))
This is a shortcut to Live Template.
Let's add two buttons:
// buttons val startBt = mDevice!!.findObject( res("${APPLICATION_ID}:id/bt_start")) val stopBt = mDevice!!.findObject( res("${APPLICATION_ID}:id/bt_stop"))
Try the same process again: start the service → stop the service → check
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)
🚴 Run:
This time, you should see that the test didn't take 10 seconds and stopped almost immediately. So this is a successful test.
🚁 9. UiAutomator - UserInput input test
👁🗨 LogOutput.kt - Log output
We can intercept some logs to check the results.
/** * 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") }
📳 Input 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() }
Run! Intercept Logcat:
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...
Test passed!
🍞 10. English version
Help, clap your hands!
English connection: 💉Inject LiveData into JobIntentService and How to🔫 Test It