Service 不一定用得很长久,那不就成了长佣了吗?我们可以用 JobIntentService ——临时佣人,它跟你的 App 同生共死,真好!但是,启动容易,关闭就毫无头绪了。因为 Service 在后台跑,跟 UI 是不沾边的。如果用 MVVM,我们可以塞 LiveData 。通过方程启动,系统会弹出 “LiveData has not initialed”。如果用 Service 的构造函数,系统会说不接受参数。饶头啊,对不?
没关系,我们可以使用插入式,我提议的是 Dagger-Hilt ,给系统打针。
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
这里随便提提,其实你们可以抄我以前写的,Gradle 太占地方了,所以省略一二。
…
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)
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()
et_message :输入资料进Service。
tv_service :Service 的反应。
JobIntentService 是 IntentService 的改良版。
这个服务是用方程启动的——enqueueWork
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(context, MyIntentService::class.java, JOB_ID, work)
}
这个 enqueueWork 有 4 种参数:
用 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()
}
}
}
你瞧,自己关自己。
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
...
android:name=".ui.MainActivity">
...
activity>
<service android:name=".service.MyIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
application>
…
// 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"
}
}
我需要提供两个观察点:
@HiltAndroidApp
class ServiceApp: Application()
…
@Module
@InstallIn(SingletonComponent::class)
object LiveDataModule {
@Provides
@Singleton
fun provideServiceStatus():
MutableLiveData<Boolean> = MutableLiveData<Boolean>()
@Provides
@Singleton
fun provideUserInput():
MutableLiveData<String> = MutableLiveData<String>()
}
简单吧,就伺候这俩。
…
@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)
}
}
主要功能是更新画面。
…
@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()
}
}
...
…
@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 {
...}
}
按键提供打开服务,关闭服务。
还有就是观察服务状态。
--------- 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...
加入 Welcome, 看看 Service 是否收到。
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...
IntentExtra 接收失败。
LiveData 成功。
右键点击 “class MyIntentService”, 然后 Alt+Insert 。
选 Junit4:
androidTest 文件夹:
@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()
}
const val START = "Service is Start..."
const val STOP = "Service is Stop!"
…
@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
糟透了,Espresso 在 Service 停止后才出来干活。因此,我们需要另一种工具来测试 —— UiAutomator 。
//UiAutomator
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
Sync。
…
@get:Rule(order = 1)
var hiltRule = HiltAndroidRule(this)
这个保留。
private var mDevice: UiDevice? = null
启动:
@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
}
加 Test :
@Test
fun test_stop_service_uiautomator() {
}
UiAutomator 找东西跟安卓是一样的:
val $var$ = mDevice!!.findObject(
res("${
APPLICATION_ID}:id/$obj$"))
这是 Live Template 捷径。
让我们加两个按键:
// 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)
跑啊:
这次,你应该看到测试没有用到 10 秒,几乎立刻就停止了。所以这是成功的测试。
我们可以截取一部分 Log 来检查成果。
/**
* 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")
}
/**
* 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()
}
跑啊!截取 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...
测试通过!
帮帮忙,拍拍手!
英文连接: Inject LiveData into JobIntentService and How to Test It