mirror of
https://github.com/home-assistant/android
synced 2024-10-01 13:53:53 +00:00
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:
parent
7d7fc0bd6e
commit
42ff5acf99
|
@ -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'
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package io.homeassistant.companion.android.data.integration
|
||||
|
||||
class IntegrationException : Exception()
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
)
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
3
data/src/test/resources/integration/empty.json
Normal file
3
data/src/test/resources/integration/empty.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
|
@ -5,4 +5,6 @@ interface IntegrationRepository {
|
|||
suspend fun registerDevice(deviceRegistration: DeviceRegistration)
|
||||
|
||||
suspend fun isRegistered(): Boolean
|
||||
|
||||
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||
}
|
||||
|
|
|
@ -5,4 +5,6 @@ interface IntegrationUseCase {
|
|||
suspend fun registerDevice(deviceRegistration: DeviceRegistration)
|
||||
|
||||
suspend fun isRegistered(): Boolean
|
||||
|
||||
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
)
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue