blob: c56d440e55edaa0c72eb92778a71a063507b462f [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.health.services.client.impl
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import android.os.Looper
import androidx.health.services.client.HealthServicesException
import androidx.health.services.client.PassiveListenerCallback
import androidx.health.services.client.PassiveListenerService
import androidx.health.services.client.data.ComparisonType.Companion.GREATER_THAN
import androidx.health.services.client.data.DataPointContainer
import androidx.health.services.client.data.DataType.Companion.CALORIES_DAILY
import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
import androidx.health.services.client.data.DataTypeCondition
import androidx.health.services.client.data.HealthEvent
import androidx.health.services.client.data.HealthEvent.Type.Companion.FALL_DETECTED
import androidx.health.services.client.data.PassiveGoal
import androidx.health.services.client.data.PassiveGoal.TriggerFrequency.Companion.ONCE
import androidx.health.services.client.data.PassiveListenerConfig
import androidx.health.services.client.data.UserActivityInfo
import androidx.health.services.client.data.UserActivityState
import androidx.health.services.client.impl.event.PassiveListenerEvent
import androidx.health.services.client.impl.internal.IStatusCallback
import androidx.health.services.client.impl.ipc.internal.ConnectionManager
import androidx.health.services.client.impl.request.CapabilitiesRequest
import androidx.health.services.client.impl.request.FlushRequest
import androidx.health.services.client.impl.request.PassiveListenerCallbackRegistrationRequest
import androidx.health.services.client.impl.request.PassiveListenerServiceRegistrationRequest
import androidx.health.services.client.impl.response.HealthEventResponse
import androidx.health.services.client.impl.response.PassiveMonitoringCapabilitiesResponse
import androidx.health.services.client.impl.response.PassiveMonitoringGoalResponse
import androidx.health.services.client.impl.response.PassiveMonitoringUpdateResponse
import androidx.health.services.client.proto.DataProto
import androidx.health.services.client.proto.DataProto.ComparisonType.COMPARISON_TYPE_GREATER_THAN
import androidx.health.services.client.proto.DataProto.PassiveGoal.TriggerFrequency.TRIGGER_FREQUENCY_ONCE
import androidx.health.services.client.proto.DataProto.UserActivityState.USER_ACTIVITY_STATE_PASSIVE
import androidx.health.services.client.proto.ResponsesProto
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
@RunWith(RobolectricTestRunner::class)
class ServiceBackedPassiveMonitoringClientTest {
private lateinit var client: ServiceBackedPassiveMonitoringClient
private lateinit var fakeService: FakeServiceStub
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Application>()
client = ServiceBackedPassiveMonitoringClient(
context, ConnectionManager(context, context.mainLooper)
)
fakeService = FakeServiceStub()
val packageName =
ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.servicePackageName
val action = ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.bindAction
shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
Intent().setPackage(packageName).setAction(action),
ComponentName(packageName, ServiceBackedPassiveMonitoringClient.CLIENT),
fakeService
)
}
@After
fun tearDown() {
client.clearPassiveListenerCallbackAsync()
client.clearPassiveListenerServiceAsync()
shadowOf(Looper.getMainLooper()).idle()
}
@Test
fun registersPassiveListenerService_success() {
val config = PassiveListenerConfig(
dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
shouldUserActivityInfoBeRequested = true,
dailyGoals = setOf(),
healthEventTypes = setOf()
)
val future = client.setPassiveListenerServiceAsync(FakeListenerService::class.java, config)
shadowOf(Looper.getMainLooper()).idle()
// Return value of future.get() is not used, but verifying no exceptions are thrown.
future.get()
assertThat(fakeService.registerServiceRequests).hasSize(1)
val request = fakeService.registerServiceRequests[0]
assertThat(request.passiveListenerConfig.dataTypes).containsExactly(
STEPS_DAILY, CALORIES_DAILY
)
assertThat(request.passiveListenerConfig.shouldUserActivityInfoBeRequested).isTrue()
assertThat(request.packageName).isEqualTo("androidx.health.services.client.test")
}
@Test
fun registersPassiveListenerService_fail() {
val config = PassiveListenerConfig(
dataTypes = setOf(CALORIES_DAILY),
shouldUserActivityInfoBeRequested = true,
dailyGoals = setOf(
PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
),
healthEventTypes = setOf()
)
val future = client.setPassiveListenerServiceAsync(FakeListenerService::class.java, config)
shadowOf(Looper.getMainLooper()).idle()
// Return value of future.get() is not used, but verifying no exceptions are thrown.
var exception: Exception? = null
try {
future.get()
} catch (e: Exception) {
exception = e
}
assertThat(exception).isNotNull()
assertThat(exception?.cause).isInstanceOf(HealthServicesException::class.java)
assertThat(exception).hasMessageThat()
.contains("Service registration failed: DataType for the requested " +
"passive goal must be tracked")
}
@Test
fun registersPassiveListenerCallback() {
val config = PassiveListenerConfig(
dataTypes = setOf(STEPS_DAILY),
shouldUserActivityInfoBeRequested = true,
dailyGoals = setOf(),
healthEventTypes = setOf()
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(1)
assertThat(callback.onRegisteredCalls).isEqualTo(1)
val request = fakeService.registerCallbackRequests[0]
assertThat(request.passiveListenerConfig.dataTypes).containsExactly(STEPS_DAILY)
assertThat(request.passiveListenerConfig.shouldUserActivityInfoBeRequested).isTrue()
assertThat(request.packageName).isEqualTo("androidx.health.services.client.test")
}
@Test
fun registersPassiveListenerCallback_fail() {
val config = PassiveListenerConfig(
dataTypes = setOf(CALORIES_DAILY),
shouldUserActivityInfoBeRequested = true,
dailyGoals = setOf(
PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
),
healthEventTypes = setOf()
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(0)
assertThat(callback.onRegistrationFailedThrowables).hasSize(1)
assertThat(callback.onRegistrationFailedThrowables[0]).hasMessageThat()
.contains("Callback registration failed: DataType for the requested " +
"passive goal must be tracked")
}
@Test
fun callbackReceivesDataPointsAndUserActivityInfo() {
shadowOf(Looper.getMainLooper()).idle() // ?????
val config = PassiveListenerConfig(
dataTypes = setOf(STEPS_DAILY),
shouldUserActivityInfoBeRequested = true,
dailyGoals = setOf(),
healthEventTypes = setOf()
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(1)
val callbackFromService = fakeService.registeredCallbacks[0]
val passiveDataPointEvent = PassiveListenerEvent.createPassiveUpdateResponse(
PassiveMonitoringUpdateResponse(
ResponsesProto.PassiveMonitoringUpdateResponse.newBuilder().setUpdate(
DataProto.PassiveMonitoringUpdate.newBuilder().addDataPoints(
DataProto.DataPoint.newBuilder().setDataType(STEPS_DAILY.proto)
.setStartDurationFromBootMs(2)
.setEndDurationFromBootMs(49)
.setValue(DataProto.Value.newBuilder().setLongVal(89)
)
).addUserActivityInfoUpdates(
DataProto.UserActivityInfo.newBuilder()
.setState(USER_ACTIVITY_STATE_PASSIVE)
)
).build()
)
)
callbackFromService.onPassiveListenerEvent(passiveDataPointEvent)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(1)
assertThat(callback.dataPointsReceived).hasSize(1)
assertThat(callback.dataPointsReceived[0].dataPoints).hasSize(1)
val stepsDataPoint = callback.dataPointsReceived[0].getData(STEPS_DAILY)[0]
assertThat(stepsDataPoint.value).isEqualTo(89)
assertThat(stepsDataPoint.dataType).isEqualTo(STEPS_DAILY)
assertThat(callback.userActivityInfosReceived).hasSize(1)
assertThat(callback.userActivityInfosReceived[0].userActivityState).isEqualTo(
UserActivityState.USER_ACTIVITY_PASSIVE
)
}
@Test
fun callbackReceivesCompletedGoals() {
val config = PassiveListenerConfig(
dataTypes = setOf(STEPS_DAILY),
shouldUserActivityInfoBeRequested = false,
dailyGoals = setOf(
PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
),
healthEventTypes = setOf()
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
val callbackFromService = fakeService.registeredCallbacks[0]
val passiveGoalEvent = PassiveListenerEvent.createPassiveGoalResponse(
PassiveMonitoringGoalResponse(
ResponsesProto.PassiveMonitoringGoalResponse.newBuilder()
.setGoal(DataProto.PassiveGoal.newBuilder()
.setTriggerFrequency(TRIGGER_FREQUENCY_ONCE)
.setCondition(DataProto.DataTypeCondition.newBuilder()
.setDataType(STEPS_DAILY.proto)
.setComparisonType(COMPARISON_TYPE_GREATER_THAN)
.setThreshold(DataProto.Value.newBuilder().setLongVal(87))
.build())
.build())
.build()
)
)
callbackFromService.onPassiveListenerEvent(passiveGoalEvent)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(1)
assertThat(callback.completedGoals).hasSize(1)
val goal = callback.completedGoals[0]
assertThat(goal.triggerFrequency).isEqualTo(ONCE)
assertThat(goal.dataTypeCondition.dataType).isEqualTo(STEPS_DAILY)
assertThat(goal.dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
assertThat(goal.dataTypeCondition.threshold).isEqualTo(87)
}
@Test
fun callbackReceivesHealthEvents() {
val config = PassiveListenerConfig(
dataTypes = setOf(),
shouldUserActivityInfoBeRequested = false,
dailyGoals = setOf(),
healthEventTypes = setOf(FALL_DETECTED)
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
val callbackFromService = fakeService.registeredCallbacks[0]
val passiveHealthEvent = PassiveListenerEvent.createHealthEventResponse(
HealthEventResponse(
ResponsesProto.HealthEventResponse.newBuilder().setHealthEvent(
DataProto.HealthEvent.newBuilder()
.setHealthEventTypeId(FALL_DETECTED.id)
.build()
).build()
)
)
callbackFromService.onPassiveListenerEvent(passiveHealthEvent)
shadowOf(Looper.getMainLooper()).idle()
assertThat(fakeService.registerCallbackRequests).hasSize(1)
assertThat(callback.healthEventsReceived).hasSize(1)
val healthEvent = callback.healthEventsReceived[0]
assertThat(healthEvent.type).isEqualTo(FALL_DETECTED)
}
@Test
fun callbackReceivesPermissionsLost() {
val config = PassiveListenerConfig(
dataTypes = setOf(STEPS_DAILY),
shouldUserActivityInfoBeRequested = false,
dailyGoals = setOf(
PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
),
healthEventTypes = setOf()
)
val callback = FakeCallback()
client.setPassiveListenerCallback(config, callback)
shadowOf(Looper.getMainLooper()).idle()
val callbackFromService = fakeService.registeredCallbacks[0]
val passiveGoalEvent = PassiveListenerEvent.createPermissionLostResponse()
callbackFromService.onPassiveListenerEvent(passiveGoalEvent)
shadowOf(Looper.getMainLooper()).idle()
assertThat(callback.onPermissionLostCalls).isEqualTo(1)
}
class FakeListenerService : PassiveListenerService()
internal class FakeCallback : PassiveListenerCallback {
var onRegisteredCalls = 0
val onRegistrationFailedThrowables = mutableListOf<Throwable>()
val dataPointsReceived = mutableListOf<DataPointContainer>()
val userActivityInfosReceived = mutableListOf<UserActivityInfo>()
val completedGoals = mutableListOf<PassiveGoal>()
val healthEventsReceived = mutableListOf<HealthEvent>()
var onPermissionLostCalls = 0
override fun onRegistered() {
onRegisteredCalls++
}
override fun onRegistrationFailed(throwable: Throwable) {
onRegistrationFailedThrowables += throwable
}
override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
dataPointsReceived += dataPoints
}
override fun onUserActivityInfoReceived(info: UserActivityInfo) {
userActivityInfosReceived += info
}
override fun onGoalCompleted(goal: PassiveGoal) {
completedGoals += goal
}
override fun onHealthEventReceived(event: HealthEvent) {
healthEventsReceived += event
}
override fun onPermissionLost() {
onPermissionLostCalls++
}
}
internal class FakeServiceStub : IPassiveMonitoringApiService.Stub() {
@JvmField
var apiVersion = 42
var statusCallbackAction: (IStatusCallback?) -> Unit = { it!!.onSuccess() }
val registerServiceRequests = mutableListOf<PassiveListenerServiceRegistrationRequest>()
val registerCallbackRequests = mutableListOf<PassiveListenerCallbackRegistrationRequest>()
val registeredCallbacks = mutableListOf<IPassiveListenerCallback>()
val unregisterServicePackageNames = mutableListOf<String>()
val unregisterCallbackPackageNames = mutableListOf<String>()
override fun getApiVersion() = 42
override fun getCapabilities(
request: CapabilitiesRequest?
): PassiveMonitoringCapabilitiesResponse {
throw NotImplementedError()
}
override fun flush(request: FlushRequest?, statusCallback: IStatusCallback?) {
throw NotImplementedError()
}
override fun registerPassiveListenerService(
request: PassiveListenerServiceRegistrationRequest,
statusCallback: IStatusCallback
) {
registerServiceRequests += request
statusCallbackAction.invoke(statusCallback)
}
override fun registerPassiveListenerCallback(
request: PassiveListenerCallbackRegistrationRequest,
callback: IPassiveListenerCallback,
statusCallback: IStatusCallback
) {
registerCallbackRequests += request
registeredCallbacks += callback
statusCallbackAction.invoke(statusCallback)
}
override fun unregisterPassiveListenerService(
packageName: String,
statusCallback: IStatusCallback
) {
unregisterServicePackageNames += packageName
statusCallbackAction.invoke(statusCallback)
}
override fun unregisterPassiveListenerCallback(
packageName: String,
statusCallback: IStatusCallback
) {
unregisterCallbackPackageNames += packageName
statusCallbackAction.invoke(statusCallback)
}
}
}