Device Tracking (#74)

* Initial work on requesting updates to location.

* Location now working in background too for Q+.

* Location now sent back to HA.
This commit is contained in:
Justin Bassett 2019-12-03 13:19:30 -05:00 committed by Cedrick Flocon
parent 7d7fc0bd6e
commit 42ff5acf99
20 changed files with 751 additions and 35 deletions

View file

@ -92,6 +92,8 @@ dependencies {
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.google.android.gms:play-services-location:17.0.0'
testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek2Version"
testImplementation "org.spekframework.spek2:spek-runner-junit5:$spek2Version"
testImplementation "org.assertj:assertj-core:$assertJVersion"
@ -99,4 +101,6 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
}
apply plugin: 'com.google.gms.google-services'
// This plugin must stay at the bottom
// https://developers.google.com/android/guides/google-services-plugin
apply plugin: 'com.google.gms.google-services'

View file

@ -5,6 +5,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:name=".HomeAssistantApplication"
@ -17,6 +21,16 @@
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
<receiver
android:name=".background.LocationBroadcastReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity android:name=".launch.LaunchActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -24,13 +38,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".onboarding.OnboardingActivity" />
<activity android:name=".webview.WebViewActivity" />
<activity android:name=".settings.SettingsActivity" />
</application>
</manifest>

View file

@ -0,0 +1,146 @@
package io.homeassistant.companion.android.background
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.BatteryManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
import io.homeassistant.companion.android.domain.integration.UpdateLocation
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class LocationBroadcastReceiver : BroadcastReceiver() {
companion object {
const val ACTION_REQUEST_LOCATION_UPDATES =
"io.homeassistant.companion.android.background.REQUEST_UPDATES"
const val ACTION_PROCESS_LOCATION =
"io.homeassistant.companion.android.background.PROCESS_UPDATES"
private const val TAG = "LocBroadcastReceiver"
}
@Inject
lateinit var integrationUseCase: IntegrationUseCase
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
override fun onReceive(context: Context, intent: Intent) {
ensureInjected(context)
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> requestUpdates(context)
ACTION_REQUEST_LOCATION_UPDATES -> requestUpdates(context)
ACTION_PROCESS_LOCATION -> handleUpdate(context, intent)
else -> Log.w(TAG, "Unknown intent action: ${intent.action}!")
}
}
private fun ensureInjected(context: Context) {
if (context.applicationContext is GraphComponentAccessor) {
DaggerReceiverComponent.builder()
.appComponent((context.applicationContext as GraphComponentAccessor).appComponent)
.build()
.inject(this)
} else {
throw Exception("Application Context passed is not of our application!")
}
}
private fun requestUpdates(context: Context) {
Log.d(TAG, "Registering for location updates.")
if (ActivityCompat.checkSelfPermission(
context,
ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.w(TAG, "Not starting location reporting because of permissions.")
return
}
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
fusedLocationProviderClient.requestLocationUpdates(
createLocationRequest(),
getLocationUpdateIntent(context)
)
}
private fun handleUpdate(context: Context, intent: Intent) {
Log.d(TAG, "Received location update.")
LocationResult.extractResult(intent)?.lastLocation?.let {
Log.d(
TAG, "Last Location: " +
"\nCoords:(${it.latitude}, ${it.longitude})" +
"\nAccuracy: ${it.accuracy}" +
"\nBearing: ${it.bearing}"
)
val updateLocation = UpdateLocation(
"",
arrayOf(it.latitude, it.longitude),
it.accuracy.toInt(),
getBatteryLevel(context),
it.speed.toInt(),
it.altitude.toInt(),
it.bearing.toInt(),
if (Build.VERSION.SDK_INT >= 26) it.verticalAccuracyMeters.toInt() else null
)
mainScope.launch {
try {
integrationUseCase.updateLocation(updateLocation)
} catch (e: Exception) {
Log.e(TAG, "Could not update location.", e)
}
}
}
}
private fun getLocationUpdateIntent(context: Context): PendingIntent {
val intent = Intent(context, LocationBroadcastReceiver::class.java)
intent.action = ACTION_PROCESS_LOCATION
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
private fun createLocationRequest(): LocationRequest {
val locationRequest = LocationRequest()
locationRequest.interval = 60000 // Every 60 seconds
locationRequest.fastestInterval = 30000 // Every 30 seconds
locationRequest.maxWaitTime = 200000 // Every 5 minutes
locationRequest.priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
return locationRequest
}
private fun getBatteryLevel(context: Context): Int? {
val batteryIntent =
context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = batteryIntent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
if (level == -1 || scale == -1) {
Log.e(TAG, "Issue getting battery level!")
return null
}
return (level.toFloat() / scale.toFloat() * 100.0f).toInt()
}
}

View file

@ -0,0 +1,10 @@
package io.homeassistant.companion.android.background
import dagger.Component
import io.homeassistant.companion.android.common.dagger.AppComponent
@Component(dependencies = [AppComponent::class])
interface ReceiverComponent {
fun inject(receiver: LocationBroadcastReceiver)
}

View file

@ -1,9 +1,11 @@
package io.homeassistant.companion.android.launch
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import io.homeassistant.companion.android.DaggerPresenterComponent
import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.background.LocationBroadcastReceiver
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.webview.WebViewActivity
@ -24,6 +26,11 @@ class LaunchActivity : AppCompatActivity(), LaunchView {
.build()
.inject(this)
val intent = Intent(this, LocationBroadcastReceiver::class.java)
intent.action = LocationBroadcastReceiver.ACTION_REQUEST_LOCATION_UPDATES
sendBroadcast(intent)
presenter.onViewReady()
}

View file

@ -1,15 +1,24 @@
package io.homeassistant.companion.android.onboarding.integration
import android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ViewFlipper
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import io.homeassistant.companion.android.DaggerPresenterComponent
import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.background.LocationBroadcastReceiver
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import javax.inject.Inject
@ -19,6 +28,8 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
private const val LOADING_VIEW = 0
private const val ERROR_VIEW = 1
private const val LOCATION_REQUEST_CODE = 1000
fun newInstance(): MobileAppIntegrationFragment {
return MobileAppIntegrationFragment()
}
@ -62,7 +73,16 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
}
override fun deviceRegistered() {
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
if (!haveLocationPermission()) {
ActivityCompat.requestPermissions(
activity!!,
getLocationPermissions(),
LOCATION_REQUEST_CODE
)
} else {
// If we have permission already we can just continue with
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
}
}
override fun registrationSkipped() {
@ -81,4 +101,44 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
presenter.onFinish()
super.onDestroy()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_REQUEST_CODE &&
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
) {
val intent = Intent(context, LocationBroadcastReceiver::class.java)
intent.action = LocationBroadcastReceiver.ACTION_REQUEST_LOCATION_UPDATES
activity!!.sendBroadcast(intent)
}
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
}
private fun haveLocationPermission(): Boolean {
return ActivityCompat.checkSelfPermission(
context!!,
ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(
context!!,
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
@SuppressLint("InlinedApi")
private fun getLocationPermissions(): Array<String> {
var retVal = arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)
if (Build.VERSION.SDK_INT >= 21)
retVal = retVal.plus(ACCESS_BACKGROUND_LOCATION)
return retVal
}
}

View file

@ -0,0 +1,3 @@
package io.homeassistant.companion.android.data.integration
class IntegrationException : Exception()

View file

@ -4,8 +4,11 @@ import io.homeassistant.companion.android.data.LocalStorage
import io.homeassistant.companion.android.domain.authentication.AuthenticationRepository
import io.homeassistant.companion.android.domain.integration.DeviceRegistration
import io.homeassistant.companion.android.domain.integration.IntegrationRepository
import io.homeassistant.companion.android.domain.integration.UpdateLocation
import javax.inject.Inject
import javax.inject.Named
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
class IntegrationRepositoryImpl @Inject constructor(
private val integrationService: IntegrationService,
@ -37,6 +40,51 @@ class IntegrationRepositoryImpl @Inject constructor(
return localStorage.getString(PREF_WEBHOOK_ID) != null
}
override suspend fun updateLocation(updateLocation: UpdateLocation) {
val updateLocationRequest = createUpdateLocation(updateLocation)
for (it in getUrls()) {
var wasSuccess = false
try {
wasSuccess = integrationService.updateLocation(it, updateLocationRequest).isSuccessful
} catch (e: Exception) {
// Ignore failure until we are out of URLS to try!
}
// if we had a successful call we can return
if (wasSuccess)
return
}
throw IntegrationException()
}
// https://developers.home-assistant.io/docs/en/app_integration_sending_data.html#short-note-on-instance-urls
private suspend fun getUrls(): Array<HttpUrl> {
val retVal = ArrayList<HttpUrl>()
val webhook = localStorage.getString(PREF_WEBHOOK_ID)
localStorage.getString(PREF_CLOUD_URL)?.let {
retVal.add(it.toHttpUrl())
}
localStorage.getString(PREF_REMOTE_UI_URL)?.let {
retVal.add(
it.toHttpUrl().newBuilder()
.addPathSegments("api/webhook/$webhook")
.build()
)
}
authenticationRepository.getUrl().toString().let {
retVal.add(
it.toHttpUrl().newBuilder()
.addPathSegments("api/webhook/$webhook")
.build()
)
}
return retVal.toTypedArray()
}
private fun createRegisterDeviceRequest(deviceRegistration: DeviceRegistration): RegisterDeviceRequest {
return RegisterDeviceRequest(
deviceRegistration.appId,
@ -51,4 +99,20 @@ class IntegrationRepositoryImpl @Inject constructor(
deviceRegistration.appData
)
}
private fun createUpdateLocation(updateLocation: UpdateLocation): IntegrationRequest {
return IntegrationRequest(
"update_location",
UpdateLocationRequest(
updateLocation.locationName,
updateLocation.gps,
updateLocation.gpsAccuracy,
updateLocation.battery,
updateLocation.speed,
updateLocation.altitude,
updateLocation.course,
updateLocation.verticalAccuracy
)
)
}
}

View file

@ -0,0 +1,9 @@
package io.homeassistant.companion.android.data.integration
import com.fasterxml.jackson.annotation.JsonInclude
data class IntegrationRequest(
val type: String,
@JsonInclude(JsonInclude.Include.NON_NULL)
val data: Any
)

View file

@ -1,8 +1,12 @@
package io.homeassistant.companion.android.data.integration
import okhttp3.HttpUrl
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
interface IntegrationService {
@ -11,4 +15,10 @@ interface IntegrationService {
@Header("Authorization") auth: String,
@Body request: RegisterDeviceRequest
): RegisterDeviceResponse
@POST
suspend fun updateLocation(
@Url url: HttpUrl,
@Body request: IntegrationRequest
): Response<ResponseBody>
}

View file

@ -0,0 +1,12 @@
package io.homeassistant.companion.android.data.integration
data class UpdateLocationRequest(
val locationName: String,
val gps: Array<Double>,
val gpsAccuracy: Int,
val battery: Int?,
val speed: Int,
val altitude: Int,
val course: Int,
val verticalAccuracy: Int?
)

View file

@ -14,6 +14,8 @@ class HomeAssistantMockService<T>(private val c: Class<T>) {
return homeAssistantRetrofit.create(c)
}
fun getMockServer() = mockServer
fun enqueueResponse(code: Int, file: String? = null) {
val mockResponse = MockResponse()
if (file != null) {

View file

@ -3,15 +3,21 @@ package io.homeassistant.companion.android.data.integration
import io.homeassistant.companion.android.data.LocalStorage
import io.homeassistant.companion.android.domain.authentication.AuthenticationRepository
import io.homeassistant.companion.android.domain.integration.DeviceRegistration
import io.homeassistant.companion.android.domain.integration.UpdateLocation
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyAll
import io.mockk.every
import io.mockk.mockk
import java.net.URL
import kotlin.properties.Delegates
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import retrofit2.Response
object IntegrationRepositoryImplSpec : Spek({
@ -91,10 +97,11 @@ object IntegrationRepositoryImplSpec : Spek({
runBlocking { isRegistered = repository.isRegistered() }
}
it("should return true when webhook has a value") {
Assertions.assertThat(isRegistered).isTrue()
assertThat(isRegistered).isTrue()
}
}
}
describe("is not registered") {
beforeEachTest {
coEvery { localStorage.getString("webhook_id") } returns null
@ -105,7 +112,308 @@ object IntegrationRepositoryImplSpec : Spek({
runBlocking { isRegistered = repository.isRegistered() }
}
it("should return false when webhook has no value") {
Assertions.assertThat(isRegistered).isFalse()
assertThat(isRegistered).isFalse()
}
}
}
describe("location updated") {
beforeEachTest {
coEvery { authenticationRepository.getUrl() } returns URL("http://example.com")
coEvery { localStorage.getString("webhook_id") } returns "FGHIJ"
}
describe("updateLocation cloud url") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
UpdateLocationRequest(
location.locationName,
location.gps,
location.gpsAccuracy,
location.battery,
location.speed,
location.altitude,
location.course,
location.verticalAccuracy
)
)
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns "http://best.com/hook/id"
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com"
coEvery {
integrationService.updateLocation(
any(), // "http://example.com/api/webhook/FGHIJ",
any() // integrationRequest
)
} returns Response.success(null)
runBlocking { repository.updateLocation(location) }
}
it("should call the service.") {
coVerify {
integrationService.updateLocation(
"http://best.com/hook/id".toHttpUrl(),
integrationRequest
)
}
}
}
describe("updateLocation remote ui url") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
UpdateLocationRequest(
location.locationName,
location.gps,
location.gpsAccuracy,
location.battery,
location.speed,
location.altitude,
location.course,
location.verticalAccuracy
)
)
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns null
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com"
coEvery {
integrationService.updateLocation(
any(), // "http://example.com/api/webhook/FGHIJ",
any() // integrationRequest
)
} returns Response.success(null)
runBlocking { repository.updateLocation(location) }
}
it("should call the service.") {
coVerify {
integrationService.updateLocation(
"http://better.com/api/webhook/FGHIJ".toHttpUrl(),
integrationRequest
)
}
}
}
describe("updateLocation auth url") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
UpdateLocationRequest(
location.locationName,
location.gps,
location.gpsAccuracy,
location.battery,
location.speed,
location.altitude,
location.course,
location.verticalAccuracy
)
)
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns null
coEvery { localStorage.getString("remote_ui_url") } returns null
coEvery {
integrationService.updateLocation(
any(), // "http://example.com/api/webhook/FGHIJ",
any() // integrationRequest
)
} returns Response.success(null)
runBlocking { repository.updateLocation(location) }
}
it("should call the service.") {
coVerify {
integrationService.updateLocation(
"http://example.com/api/webhook/FGHIJ".toHttpUrl(),
integrationRequest
)
}
}
}
describe("updateLocation fail then succeeds") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
UpdateLocationRequest(
location.locationName,
location.gps,
location.gpsAccuracy,
location.battery,
location.speed,
location.altitude,
location.course,
location.verticalAccuracy
)
)
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns "http://best.com/hook/id"
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com"
coEvery {
integrationService.updateLocation(
"http://best.com/hook/id".toHttpUrl(),
any() // integrationRequest
)
} returns mockk {
every { isSuccessful } returns false
}
coEvery {
integrationService.updateLocation(
"http://better.com/api/webhook/FGHIJ".toHttpUrl(),
any() // integrationRequest
)
} returns Response.success(null)
runBlocking { repository.updateLocation(location) }
}
it("should call service 2 times") {
coVerifyAll {
integrationService.updateLocation(
"http://best.com/hook/id".toHttpUrl(),
integrationRequest
)
integrationService.updateLocation(
"http://better.com/api/webhook/FGHIJ".toHttpUrl(),
integrationRequest
)
}
}
}
describe("updateLocation failure") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
lateinit var thrown: Throwable
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns "http://best.com/hook/id"
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com"
coEvery {
integrationService.updateLocation(
"http://best.com/hook/id".toHttpUrl(),
any() // integrationRequest
)
} returns mockk {
every { isSuccessful } returns false
}
coEvery {
integrationService.updateLocation(
"http://better.com/api/webhook/FGHIJ".toHttpUrl(),
any() // integrationRequest
)
} returns mockk {
every { isSuccessful } returns false
}
coEvery {
integrationService.updateLocation(
"http://example.com/api/webhook/FGHIJ".toHttpUrl(),
any() // integrationRequest
)
} returns mockk {
every { isSuccessful } returns false
}
thrown = catchThrowable { runBlocking { repository.updateLocation(location) } }
}
it("should throw an exception") {
assertThat(thrown).isInstanceOf(IntegrationException::class.java)
}
}
describe("updateLocation with trailing slash") {
val location = UpdateLocation(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
UpdateLocationRequest(
location.locationName,
location.gps,
location.gpsAccuracy,
location.battery,
location.speed,
location.altitude,
location.course,
location.verticalAccuracy
)
)
beforeEachTest {
coEvery { localStorage.getString("cloud_url") } returns null
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com/"
coEvery {
integrationService.updateLocation(
any(), // "http://example.com/api/webhook/FGHIJ",
any() // integrationRequest
)
} returns Response.success(null)
runBlocking { repository.updateLocation(location) }
}
it("should call the service.") {
coVerify {
integrationService.updateLocation(
"http://better.com/api/webhook/FGHIJ".toHttpUrl(),
integrationRequest
)
}
}
}
}

View file

@ -2,45 +2,82 @@ package io.homeassistant.companion.android.data.integration
import io.homeassistant.companion.android.data.HomeAssistantMockService
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import okhttp3.mockwebserver.RecordedRequest
import org.assertj.core.api.Assertions.assertThat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import retrofit2.Response
object IntegrationServiceSpec : Spek({
describe("an integration service") {
val mockService by memoized { HomeAssistantMockService(IntegrationService::class.java) }
lateinit var request: RecordedRequest
lateinit var registrationResponse: RegisterDeviceResponse
describe("registerDevice") {
lateinit var registrationResponse: RegisterDeviceResponse
beforeEachTest {
val registrationRequest = RegisterDeviceRequest(
"appId",
"appName",
"appVersion",
"deviceName",
"manufacturer",
"model",
"osName",
"osVersion",
false,
null
)
mockService.enqueueResponse(200, "integration/register.json")
registrationResponse = runBlocking {
mockService.get().registerDevice("123", registrationRequest)
beforeEachTest {
val registrationRequest = RegisterDeviceRequest(
"appId",
"appName",
"appVersion",
"deviceName",
"manufacturer",
"model",
"osName",
"osVersion",
false,
null
)
mockService.enqueueResponse(200, "integration/register.json")
registrationResponse = runBlocking {
mockService.get().registerDevice("123", registrationRequest)
}
request = mockService.takeRequest()
}
it("should serialize request") {
assertThat(request.method).isEqualTo("POST")
assertThat(request.path).isEqualTo("/api/mobile_app/registrations")
assertThat(request.body.readUtf8())
.contains("\"app_id\":\"appId\"")
}
it("should deserialize the response") {
assertThat(registrationResponse.webhookId).isEqualTo("ABC")
}
request = mockService.takeRequest()
}
it("should serialize request") {
assertThat(request.method).isEqualTo("POST")
assertThat(request.path).isEqualTo("/api/mobile_app/registrations")
assertThat(request.body.readUtf8())
.contains("\"app_id\":\"appId\"")
}
it("should deserialize the response") {
assertThat(registrationResponse.webhookId).isEqualTo("ABC")
describe("updateLocation") {
lateinit var response: Response<ResponseBody>
beforeEachTest {
val updateLocationRequest = UpdateLocationRequest(
"locationName",
arrayOf(45.0, -45.0),
0,
1,
2,
3,
4,
5
)
val integrationRequest = IntegrationRequest(
"update_location",
updateLocationRequest
)
mockService.enqueueResponse(200, "integration/empty.json")
response = runBlocking {
mockService.get().updateLocation(mockService.getMockServer().url("/path/to/hook"), integrationRequest)
}
request = mockService.takeRequest()
}
it("should serialize request") {
assertThat(request.method).isEqualTo("POST")
assertThat(request.path).isEqualTo("/path/to/hook")
}
it("should return success") {
assertThat(response.isSuccessful).isTrue()
}
}
}
})

View file

@ -0,0 +1,3 @@
{
}

View file

@ -5,4 +5,6 @@ interface IntegrationRepository {
suspend fun registerDevice(deviceRegistration: DeviceRegistration)
suspend fun isRegistered(): Boolean
suspend fun updateLocation(updateLocation: UpdateLocation)
}

View file

@ -5,4 +5,6 @@ interface IntegrationUseCase {
suspend fun registerDevice(deviceRegistration: DeviceRegistration)
suspend fun isRegistered(): Boolean
suspend fun updateLocation(updateLocation: UpdateLocation)
}

View file

@ -12,4 +12,8 @@ class IntegrationUseCaseImpl @Inject constructor(
override suspend fun isRegistered(): Boolean {
return integrationRepository.isRegistered()
}
override suspend fun updateLocation(updateLocation: UpdateLocation) {
return integrationRepository.updateLocation(updateLocation)
}
}

View file

@ -0,0 +1,12 @@
package io.homeassistant.companion.android.domain.integration
data class UpdateLocation(
val locationName: String,
val gps: Array<Double>,
val gpsAccuracy: Int,
val battery: Int?,
val speed: Int,
val altitude: Int,
val course: Int,
val verticalAccuracy: Int?
)

View file

@ -48,5 +48,16 @@ object IntegrationUseCaseImplSpec : Spek({
coVerify { integrationRepository.isRegistered() }
}
}
describe("updateLocation") {
val location = mockk<UpdateLocation>()
beforeEachTest {
runBlocking { useCase.updateLocation(location) }
}
it("should call the repository") {
coVerify { integrationRepository.updateLocation(location) }
}
}
}
})