mirror of
https://github.com/home-assistant/android
synced 2024-10-01 13:53:53 +00:00
Feature/location settings (#129)
* Initial work on zone based location tracking. * Tests and enhancements. * Fix unit tests. * Fix test cases. * Initial location preferences work. * Location settings now function will requests for permissions when needed. * ktlink formatting. * Domain tests. * Data tests * Extract strings into resources. * Remove translations until Lokalise SDK is added back. Failing lint * Update wording. * Add icons and toolbar. * Coloring the settings.
This commit is contained in:
parent
0e515a8222
commit
594936c088
|
@ -83,8 +83,9 @@ dependencies {
|
|||
kapt "com.google.dagger:dagger-compiler:${daggerVersion}"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appCompatVersion"
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutversion"
|
||||
implementation "androidx.preference:preference-ktx:1.1.0"
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation("com.jakewharton.threetenabp:threetenabp:$threeTenAbpVersion") {
|
||||
exclude group: 'org.threeten'
|
||||
|
|
|
@ -27,7 +27,9 @@
|
|||
android:exported="true"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
@ -40,7 +42,9 @@
|
|||
</activity>
|
||||
<activity android:name=".onboarding.OnboardingActivity" />
|
||||
<activity android:name=".webview.WebViewActivity" />
|
||||
<activity android:name=".settings.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".webview.WebViewActivity"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -6,6 +6,8 @@ import io.homeassistant.companion.android.launch.LaunchActivity
|
|||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationFragment
|
||||
import io.homeassistant.companion.android.onboarding.manual.ManualSetupFragment
|
||||
import io.homeassistant.companion.android.settings.SettingsActivity
|
||||
import io.homeassistant.companion.android.settings.SettingsFragment
|
||||
import io.homeassistant.companion.android.webview.WebViewActivity
|
||||
|
||||
@Component(dependencies = [AppComponent::class], modules = [PresenterModule::class])
|
||||
|
@ -19,5 +21,9 @@ interface PresenterComponent {
|
|||
|
||||
fun inject(fragment: MobileAppIntegrationFragment)
|
||||
|
||||
fun inject(activity: SettingsActivity)
|
||||
|
||||
fun inject(fragment: SettingsFragment)
|
||||
|
||||
fun inject(activity: WebViewActivity)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegr
|
|||
import io.homeassistant.companion.android.onboarding.manual.ManualSetupPresenter
|
||||
import io.homeassistant.companion.android.onboarding.manual.ManualSetupPresenterImpl
|
||||
import io.homeassistant.companion.android.onboarding.manual.ManualSetupView
|
||||
import io.homeassistant.companion.android.settings.SettingsPresenter
|
||||
import io.homeassistant.companion.android.settings.SettingsPresenterImpl
|
||||
import io.homeassistant.companion.android.settings.SettingsView
|
||||
import io.homeassistant.companion.android.webview.WebView
|
||||
import io.homeassistant.companion.android.webview.WebViewPresenter
|
||||
import io.homeassistant.companion.android.webview.WebViewPresenterImpl
|
||||
|
@ -26,6 +29,7 @@ class PresenterModule {
|
|||
private lateinit var authenticationView: AuthenticationView
|
||||
private lateinit var manualSetupView: ManualSetupView
|
||||
private lateinit var mobileAppIntegrationView: MobileAppIntegrationView
|
||||
private lateinit var settingsView: SettingsView
|
||||
private lateinit var webView: WebView
|
||||
|
||||
constructor(launchView: LaunchView) {
|
||||
|
@ -44,6 +48,10 @@ class PresenterModule {
|
|||
this.mobileAppIntegrationView = mobileAppIntegrationView
|
||||
}
|
||||
|
||||
constructor(settingsView: SettingsView) {
|
||||
this.settingsView = settingsView
|
||||
}
|
||||
|
||||
constructor(webView: WebView) {
|
||||
this.webView = webView
|
||||
}
|
||||
|
@ -60,6 +68,9 @@ class PresenterModule {
|
|||
@Provides
|
||||
fun provideMobileAppIntegrationView() = mobileAppIntegrationView
|
||||
|
||||
@Provides
|
||||
fun provideSettingsView() = settingsView
|
||||
|
||||
@Provides
|
||||
fun provideWebView() = webView
|
||||
|
||||
|
@ -78,6 +89,9 @@ class PresenterModule {
|
|||
@Binds
|
||||
fun bindMobileAppPresenter(presenter: MobileAppIntegrationPresenterImpl): MobileAppIntegrationPresenter
|
||||
|
||||
@Binds
|
||||
fun bindSettingsPresenter(presenter: SettingsPresenterImpl): SettingsPresenter
|
||||
|
||||
@Binds
|
||||
fun bindWebViewPresenterImpl(presenter: WebViewPresenterImpl): WebViewPresenter
|
||||
}
|
||||
|
|
|
@ -7,10 +7,14 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.google.android.gms.location.Geofence
|
||||
import com.google.android.gms.location.GeofencingEvent
|
||||
import com.google.android.gms.location.GeofencingRequest
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
import com.google.android.gms.location.LocationServices
|
||||
|
@ -30,6 +34,8 @@ class LocationBroadcastReceiver : BroadcastReceiver() {
|
|||
"io.homeassistant.companion.android.background.REQUEST_UPDATES"
|
||||
const val ACTION_PROCESS_LOCATION =
|
||||
"io.homeassistant.companion.android.background.PROCESS_UPDATES"
|
||||
const val ACTION_PROCESS_GEO =
|
||||
"io.homeassistant.companion.android.background.PROCESS_GEOFENCE"
|
||||
|
||||
private const val TAG = "LocBroadcastReceiver"
|
||||
}
|
||||
|
@ -43,9 +49,10 @@ class LocationBroadcastReceiver : BroadcastReceiver() {
|
|||
ensureInjected(context)
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> requestUpdates(context)
|
||||
ACTION_REQUEST_LOCATION_UPDATES -> requestUpdates(context)
|
||||
ACTION_PROCESS_LOCATION -> handleUpdate(context, intent)
|
||||
Intent.ACTION_BOOT_COMPLETED -> setupLocationTracking(context)
|
||||
ACTION_REQUEST_LOCATION_UPDATES -> setupLocationTracking(context)
|
||||
ACTION_PROCESS_LOCATION -> handleLocationUpdate(context, intent)
|
||||
ACTION_PROCESS_GEO -> handleGeoUpdate(context, intent)
|
||||
else -> Log.w(TAG, "Unknown intent action: ${intent.action}!")
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +68,7 @@ class LocationBroadcastReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun requestUpdates(context: Context) {
|
||||
Log.d(TAG, "Registering for location updates.")
|
||||
|
||||
private fun setupLocationTracking(context: Context) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
ACCESS_COARSE_LOCATION
|
||||
|
@ -73,48 +78,88 @@ class LocationBroadcastReceiver : BroadcastReceiver() {
|
|||
return
|
||||
}
|
||||
|
||||
mainScope.launch {
|
||||
if (integrationUseCase.isBackgroundTrackingEnabled())
|
||||
requestLocationUpdates(context)
|
||||
if (integrationUseCase.isZoneTrackingEnabled())
|
||||
requestZoneUpdates(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLocationUpdates(context: Context) {
|
||||
Log.d(TAG, "Registering for location updates.")
|
||||
|
||||
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
|
||||
val intent = getLocationUpdateIntent(context, false)
|
||||
|
||||
fusedLocationProviderClient.removeLocationUpdates(intent)
|
||||
|
||||
fusedLocationProviderClient.requestLocationUpdates(
|
||||
createLocationRequest(),
|
||||
getLocationUpdateIntent(context)
|
||||
intent
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUpdate(context: Context, intent: Intent) {
|
||||
private suspend fun requestZoneUpdates(context: Context) {
|
||||
Log.d(TAG, "Registering for zone based location updates")
|
||||
|
||||
val geofencingClient = LocationServices.getGeofencingClient(context)
|
||||
val intent = getLocationUpdateIntent(context, true)
|
||||
geofencingClient.removeGeofences(intent)
|
||||
geofencingClient.addGeofences(
|
||||
createGeofencingRequest(),
|
||||
intent
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleLocationUpdate(context: Context, intent: Intent) {
|
||||
Log.d(TAG, "Received location update.")
|
||||
LocationResult.extractResult(intent)?.lastLocation?.let {
|
||||
sendLocationUpdate(it, context)
|
||||
}
|
||||
}
|
||||
|
||||
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 0
|
||||
)
|
||||
private fun handleGeoUpdate(context: Context, intent: Intent) {
|
||||
Log.d(TAG, "Received geofence update.")
|
||||
val geofencingEvent = GeofencingEvent.fromIntent(intent)
|
||||
if (geofencingEvent.hasError()) {
|
||||
Log.e(TAG, "Error getting geofence broadcast status code: ${geofencingEvent.errorCode}")
|
||||
return
|
||||
}
|
||||
|
||||
mainScope.launch {
|
||||
try {
|
||||
integrationUseCase.updateLocation(updateLocation)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not update location.", e)
|
||||
}
|
||||
sendLocationUpdate(geofencingEvent.triggeringLocation, context)
|
||||
}
|
||||
|
||||
private fun sendLocationUpdate(location: Location, context: Context) {
|
||||
Log.d(
|
||||
TAG, "Last Location: " +
|
||||
"\nCoords:(${location.latitude}, ${location.longitude})" +
|
||||
"\nAccuracy: ${location.accuracy}" +
|
||||
"\nBearing: ${location.bearing}"
|
||||
)
|
||||
val updateLocation = UpdateLocation(
|
||||
"",
|
||||
arrayOf(location.latitude, location.longitude),
|
||||
location.accuracy.toInt(),
|
||||
getBatteryLevel(context),
|
||||
location.speed.toInt(),
|
||||
location.altitude.toInt(),
|
||||
location.bearing.toInt(),
|
||||
if (Build.VERSION.SDK_INT >= 26) location.verticalAccuracyMeters.toInt() else 0
|
||||
)
|
||||
|
||||
mainScope.launch {
|
||||
try {
|
||||
integrationUseCase.updateLocation(updateLocation)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not update location.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocationUpdateIntent(context: Context): PendingIntent {
|
||||
private fun getLocationUpdateIntent(context: Context, isGeofence: Boolean): PendingIntent {
|
||||
val intent = Intent(context, LocationBroadcastReceiver::class.java)
|
||||
intent.action = ACTION_PROCESS_LOCATION
|
||||
intent.action = if (isGeofence) ACTION_PROCESS_GEO else ACTION_PROCESS_LOCATION
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
|
@ -130,6 +175,22 @@ class LocationBroadcastReceiver : BroadcastReceiver() {
|
|||
return locationRequest
|
||||
}
|
||||
|
||||
private suspend fun createGeofencingRequest(): GeofencingRequest {
|
||||
val geofencingRequestBuilder = GeofencingRequest.Builder()
|
||||
integrationUseCase.getZones().forEach {
|
||||
geofencingRequestBuilder.addGeofence(Geofence.Builder()
|
||||
.setRequestId(it.entityId)
|
||||
.setCircularRegion(
|
||||
it.attributes.latitude,
|
||||
it.attributes.longitude,
|
||||
it.attributes.radius)
|
||||
.setExpirationDuration(Geofence.NEVER_EXPIRE)
|
||||
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
|
||||
.build())
|
||||
}
|
||||
return geofencingRequestBuilder.build()
|
||||
}
|
||||
|
||||
private fun getBatteryLevel(context: Context): Int? {
|
||||
val batteryIntent =
|
||||
context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
|
|
|
@ -1,25 +1,17 @@
|
|||
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 io.homeassistant.companion.android.util.PermissionManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
|
||||
|
@ -28,8 +20,6 @@ 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()
|
||||
}
|
||||
|
@ -70,11 +60,8 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
|
|||
}
|
||||
|
||||
override fun deviceRegistered() {
|
||||
if (!haveLocationPermission()) {
|
||||
requestPermissions(
|
||||
getLocationPermissions(),
|
||||
LOCATION_REQUEST_CODE
|
||||
)
|
||||
if (!PermissionManager.haveLocationPermissions(context!!)) {
|
||||
PermissionManager.requestLocationPermissions(this)
|
||||
} else {
|
||||
// If we have permission already we can just continue with
|
||||
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
|
||||
|
@ -105,36 +92,10 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
|
|||
) {
|
||||
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)
|
||||
if (PermissionManager.validateLocationPermissions(requestCode, permissions, grantResults)) {
|
||||
presenter.onGrantedLocationPermission(context!!, activity!!)
|
||||
}
|
||||
|
||||
(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
interface MobileAppIntegrationPresenter {
|
||||
fun onRegistrationAttempt()
|
||||
fun onGrantedLocationPermission(context: Context, activity: Activity)
|
||||
fun onSkip()
|
||||
fun onFinish()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.domain.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
|
||||
import io.homeassistant.companion.android.util.PermissionManager
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -50,6 +53,14 @@ class MobileAppIntegrationPresenterImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onGrantedLocationPermission(context: Context, activity: Activity) {
|
||||
mainScope.launch {
|
||||
integrationUseCase.setZoneTrackingEnabled(true)
|
||||
integrationUseCase.setBackgroundTrackingEnabled(true)
|
||||
PermissionManager.restartLocationTracking(context, activity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSkip() {
|
||||
view.registrationSkipped()
|
||||
}
|
||||
|
|
|
@ -3,9 +3,7 @@ package io.homeassistant.companion.android.settings
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.R
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
@ -19,7 +17,11 @@ class SettingsActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
findViewById<TextView>(R.id.version_text_view).text =
|
||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content, SettingsFragment.newInstance())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreference
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.util.PermissionManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: SettingsPresenter
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((activity?.application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
preferenceManager.preferenceDataStore = presenter.getPreferenceDataStore()
|
||||
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
findPreference<Preference>("version").let {
|
||||
it!!.summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationSettingChanged() {
|
||||
if (!PermissionManager.haveLocationPermissions(context!!)) {
|
||||
PermissionManager.requestLocationPermissions(this)
|
||||
}
|
||||
PermissionManager.restartLocationTracking(context!!, activity!!)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (PermissionManager.validateLocationPermissions(requestCode, permissions, grantResults)) {
|
||||
PermissionManager.restartLocationTracking(context!!, activity!!)
|
||||
} else {
|
||||
// If we don't have permissions, don't let them in!
|
||||
findPreference<SwitchPreference>("location_zone")!!.isChecked = false
|
||||
findPreference<SwitchPreference>("location_background")!!.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = SettingsFragment()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
interface SettingsPresenter {
|
||||
fun getPreferenceDataStore(): PreferenceDataStore
|
||||
fun onFinish()
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SettingsPresenterImpl @Inject constructor(
|
||||
private val settingsView: SettingsView,
|
||||
private val integrationUseCase: IntegrationUseCase
|
||||
) : SettingsPresenter, PreferenceDataStore() {
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
return runBlocking {
|
||||
return@runBlocking when (key) {
|
||||
"location_zone" -> integrationUseCase.isZoneTrackingEnabled()
|
||||
"location_background" -> integrationUseCase.isBackgroundTrackingEnabled()
|
||||
else -> throw Exception()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
mainScope.launch {
|
||||
when (key) {
|
||||
"location_zone" -> integrationUseCase.setZoneTrackingEnabled(value)
|
||||
"location_background" -> integrationUseCase.setBackgroundTrackingEnabled(value)
|
||||
else -> throw Exception()
|
||||
}
|
||||
settingsView.onLocationSettingChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPreferenceDataStore(): PreferenceDataStore {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
interface SettingsView {
|
||||
fun onLocationSettingChanged()
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package io.homeassistant.companion.android.util
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.homeassistant.companion.android.background.LocationBroadcastReceiver
|
||||
|
||||
class PermissionManager {
|
||||
|
||||
companion object {
|
||||
private const val LOCATION_REQUEST_CODE = 1
|
||||
|
||||
fun haveLocationPermissions(context: Context): Boolean {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED &&
|
||||
ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getLocationPermissionArray(): Array<String> {
|
||||
var retVal = arrayOf(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21)
|
||||
retVal = retVal.plus(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
fun validateLocationPermissions(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
return requestCode == LOCATION_REQUEST_CODE && grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
fun requestLocationPermissions(fragment: Fragment) {
|
||||
fragment.requestPermissions(getLocationPermissionArray(), LOCATION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
fun restartLocationTracking(context: Context, activity: Activity) {
|
||||
val intent = Intent(context, LocationBroadcastReceiver::class.java)
|
||||
intent.action = LocationBroadcastReceiver.ACTION_REQUEST_LOCATION_UPDATES
|
||||
|
||||
activity.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
8
app/src/main/res/drawable/map_marker.xml
Normal file
8
app/src/main/res/drawable/map_marker.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!-- drawable/map-marker.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="@color/colorAccent" android:pathData="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z" />
|
||||
</vector>
|
8
app/src/main/res/drawable/map_marker_radius.xml
Normal file
8
app/src/main/res/drawable/map_marker_radius.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!-- drawable/map-marker-radius.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="@color/colorAccent" android:pathData="M12,2C15.31,2 18,4.66 18,7.95C18,12.41 12,19 12,19C12,19 6,12.41 6,7.95C6,4.66 8.69,2 12,2M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M20,19C20,21.21 16.42,23 12,23C7.58,23 4,21.21 4,19C4,17.71 5.22,16.56 7.11,15.83L7.75,16.74C6.67,17.19 6,17.81 6,18.5C6,19.88 8.69,21 12,21C15.31,21 18,19.88 18,18.5C18,17.81 17.33,17.19 16.25,16.74L16.89,15.83C18.78,16.56 20,17.71 20,19Z" />
|
||||
</vector>
|
|
@ -1,39 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.HomeAssistant.Headline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="36dp"
|
||||
android:text="@string/app_name"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||
app:srcCompat="@drawable/app_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/version_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/icon"
|
||||
tools:text="1.0.0 (1)" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:titleTextColor="@android:color/white"
|
||||
android:elevation="4dp" />
|
||||
<FrameLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Ricerca Home Assistant in corso.
|
||||
Scansione della rete</string>
|
||||
<string name="home_assistant_not_discover">Impossibile trovate
|
||||
il tuo Home Assistant</string>
|
||||
<string name="connect_to_home_internet">Assicurati che il telefono sia collegato
|
||||
a Internet di casa tua.</string>
|
||||
<string name="scan_again">Cerca di nuovo</string>
|
||||
<string name="manual_setup">inserisci l\'indirizzo manualmente</string>
|
||||
<string name="input_url">URL Home Assistant</string>
|
||||
<string name="input_url_hint">https://esempio.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Stato integrazione della mobile app:</string>
|
||||
<string name="checking_with_home_assistant">Verifica Home Assistant in corso</string>
|
||||
<string name="skip">Salta</string>
|
||||
<string name="retry">Riprova</string>
|
||||
<string name="attempting_registration">Tentativo di registrazione dell\'applicazione...</string>
|
||||
<string name="unable_to_register">Impossibile registrare l\'applicazione</string>
|
||||
<string name="error_with_registration">Varifica di avere l\'integrazione mobile_app abilitata
|
||||
sul tuo home assistant.</string>
|
||||
<string name="url_parse_error">Impossibile analizzare l\'URL di Home Assistant. Dovrebbe somigliare a https://esempio.com</string>
|
||||
</resources>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Netwerk scannen naar Home Assistant</string>
|
||||
<string name="home_assistant_not_discover">Home Assistant instantie niet gevonden</string>
|
||||
<string name="connect_to_home_internet">Je telefoon dient verbonden te zijn met je thuis netwerk.</string>
|
||||
<string name="scan_again">opnieuw scannen</string>
|
||||
<string name="manual_setup">Adres handmatig invullen</string>
|
||||
<string name="input_url">Home Assistant adres</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Status van mobiele app integratie:</string>
|
||||
<string name="checking_with_home_assistant">Controleren met Home Assistant</string>
|
||||
<string name="skip">Overslaan</string>
|
||||
<string name="retry">Opnieuw proberen</string>
|
||||
<string name="attempting_registration">Proberen om de applicatie te registreren...</string>
|
||||
<string name="unable_to_register">Niet mogelijk om de applicatie te registeren.</string>
|
||||
<string name="error_with_registration">Controleer of de mobile_app integratie is ingeschakeld in je Home Assistant instantie.</string>
|
||||
<string name="url_parse_error">Je Home Assistant URL lijkt niet te kloppen. Het zou ongeveer hetzelfde moeten zijn als https://example.com</string>
|
||||
</resources>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Skanowanie sieci w poszukiwaniu Home Assistant\'a</string>
|
||||
<string name="home_assistant_not_discover">Nie można znaleźć Twojego Home Assistant\'a</string>
|
||||
<string name="connect_to_home_internet">Upewnij się, że telefon jest podłączony do sieci domowej.</string>
|
||||
<string name="scan_again">skanuj ponownie</string>
|
||||
<string name="manual_setup">wprowadź adres ręcznie</string>
|
||||
<string name="input_url">Adres URL Home Assistant\'a</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Status integracji mobile_app:</string>
|
||||
<string name="checking_with_home_assistant">Sprawdzanie z Home Assistant\'em</string>
|
||||
<string name="skip">Pomiń</string>
|
||||
<string name="retry">Ponów próbę</string>
|
||||
<string name="attempting_registration">Próba zarejestrowania aplikacji…</string>
|
||||
<string name="unable_to_register">Nie można zarejestrować aplikacji</string>
|
||||
<string name="error_with_registration">Sprawdź, czy integracja mobile_app jest włączona w Twoim instancji Home Assistant\'a.</string>
|
||||
<string name="url_parse_error">Nie można rozpoznać adresu URL Home Assistant\'a. Powinien on wyglądać następująco https://example.com</string>
|
||||
</resources>
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Se caută o instanță a Home Assistant-ul in rețeaua locală </string>
|
||||
<string name="home_assistant_not_discover">Nu am găsit instanța Home Assistant-ului</string>
|
||||
<string name="connect_to_home_internet">Asigurați-vă că telefonul dvs. este conectat
|
||||
la rețeaua locală.</string>
|
||||
<string name="scan_again">scaneaza inca o data</string>
|
||||
<string name="manual_setup">ntroduceți manual adresa</string>
|
||||
<string name="input_url">Adresa Home Assistant-</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Statusul integrării a l aplicatiei mobile.</string>
|
||||
<string name="checking_with_home_assistant">Verific cu Home Assistant-ul</string>
|
||||
<string name="skip">Nu acum </string>
|
||||
<string name="retry">Reîncercați</string>
|
||||
<string name="attempting_registration">Se încearcă înregistrarea aplicației...</string>
|
||||
<string name="unable_to_register">Nu s-au putut înregistra </string>
|
||||
<string name="error_with_registration">Te rog verifică dacă este activată integrarea pentru mobile_app în configurația Home Assistant-ului.</string>
|
||||
<string name="url_parse_error">Nu am putut analiza adresa Home Assistant-ului. Ar trebui sa arate așa: https:///example.com</string>
|
||||
</resources>
|
|
@ -2,4 +2,5 @@
|
|||
<resources>
|
||||
<color name="colorPrimary">#03A9F4</color>
|
||||
<color name="colorPrimaryDark">#0288D1</color>
|
||||
<color name="colorAccent">#03A9F4</color>
|
||||
</resources>
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Scanning the network
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="scanning_network">Scanning the network
|
||||
for Home Assistant</string>
|
||||
<string name="home_assistant_not_discover">Unable to find your
|
||||
<string name="home_assistant_not_discover">Unable to find your
|
||||
Home Assistant instance</string>
|
||||
<string name="connect_to_home_internet">Make sure your phone is connected
|
||||
<string name="connect_to_home_internet">Make sure your phone is connected
|
||||
to your home internet.</string>
|
||||
<string name="scan_again">scan again</string>
|
||||
<string name="manual_setup">enter address manually</string>
|
||||
<string name="input_url">Home Assistant URL</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Status of mobile app integration:</string>
|
||||
<string name="checking_with_home_assistant">Checking with Home Assistant</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="attempting_registration">Attempting to register application…</string>
|
||||
<string name="unable_to_register">Unable to Register Application</string>
|
||||
<string name="error_with_registration">Please check to ensure you have the mobile_app
|
||||
<string name="scan_again">scan again</string>
|
||||
<string name="manual_setup">enter address manually</string>
|
||||
<string name="input_url">Home Assistant URL</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="status_of_mobile_app_integration">Status of mobile app integration:</string>
|
||||
<string name="checking_with_home_assistant">Checking with Home Assistant</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="attempting_registration">Attempting to register application…</string>
|
||||
<string name="unable_to_register">Unable to Register Application</string>
|
||||
<string name="error_with_registration">Please check to ensure you have the mobile_app
|
||||
integration enabled on your home assistant instance.</string>
|
||||
<string name="url_parse_error">Unable to parse your Home Assistant URL. It should look like https://example.com</string>
|
||||
<string name="url_parse_error">Unable to parse your Home Assistant URL. It should look like https://example.com</string>
|
||||
<string name="pref_location_zone_title">Zone Based Tracking</string>
|
||||
<string name="pref_location_zone_summary">Import existing Home Assistant zones as geofences for zone based tracking.</string>
|
||||
<string name="pref_location_background_title">Background Location Tracking</string>
|
||||
<string name="pref_location_background_summary">Update your location behind the scenes, periodically.</string>
|
||||
<string name="application_version">Application Version</string>
|
||||
</resources>
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
<style name="Theme.HomeAssistant" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimary</item>
|
||||
|
||||
<item name="colorControlNormal">@android:color/white</item>
|
||||
|
||||
<item name="buttonStyle">@style/Widget.HomeAssistant.Button.Colored</item>
|
||||
|
||||
<item name="alertDialogTheme">@style/Theme.HomeAssistant.Dialog.Alert</item>
|
||||
|
|
26
app/src/main/res/xml/preferences.xml
Normal file
26
app/src/main/res/xml/preferences.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:title="Location">
|
||||
<SwitchPreference
|
||||
android:key="location_zone"
|
||||
android:icon="@drawable/map_marker_radius"
|
||||
android:title="@string/pref_location_zone_title"
|
||||
android:summary="@string/pref_location_zone_summary"/>
|
||||
<SwitchPreference
|
||||
android:key="location_background"
|
||||
android:icon="@drawable/map_marker"
|
||||
android:title="@string/pref_location_background_title"
|
||||
android:summary="@string/pref_location_background_summary"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="App Version Info">
|
||||
<Preference
|
||||
android:key="version"
|
||||
android:icon="@drawable/app_icon"
|
||||
android:title="@string/application_version"
|
||||
android:summary="1.0.0 (1)"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -28,4 +28,12 @@ class LocalStorageImpl(private val sharedPreferences: SharedPreferences) : Local
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putBoolean(key: String, value: Boolean) {
|
||||
sharedPreferences.edit().putBoolean(key, value).apply()
|
||||
}
|
||||
|
||||
override suspend fun getBoolean(key: String): Boolean {
|
||||
return sharedPreferences.getBoolean(key, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,8 @@ interface LocalStorage {
|
|||
suspend fun putLong(key: String, value: Long?)
|
||||
|
||||
suspend fun getLong(key: String): Long?
|
||||
|
||||
suspend fun putBoolean(key: String, value: Boolean)
|
||||
|
||||
suspend fun getBoolean(key: String): Boolean
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package io.homeassistant.companion.android.data.integration
|
||||
|
||||
import java.util.Calendar
|
||||
|
||||
data class EntityResponse<T>(
|
||||
val entityId: String,
|
||||
val state: String,
|
||||
val attributes: T,
|
||||
val lastChanged: Calendar,
|
||||
val lastUpdated: Calendar,
|
||||
val context: Map<String, Any>
|
||||
)
|
|
@ -3,8 +3,10 @@ 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.Entity
|
||||
import io.homeassistant.companion.android.domain.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.domain.integration.UpdateLocation
|
||||
import io.homeassistant.companion.android.domain.integration.ZoneAttributes
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import okhttp3.HttpUrl
|
||||
|
@ -21,6 +23,9 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
private const val PREF_REMOTE_UI_URL = "remote_ui_url"
|
||||
private const val PREF_SECRET = "secret"
|
||||
private const val PREF_WEBHOOK_ID = "webhook_id"
|
||||
|
||||
private const val PREF_ZONE_ENABLED = "zone_enabled"
|
||||
private const val PREF_BACKGROUND_ENABLED = "background_enabled"
|
||||
}
|
||||
|
||||
override suspend fun registerDevice(deviceRegistration: DeviceRegistration) {
|
||||
|
@ -45,7 +50,8 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
for (it in getUrls()) {
|
||||
var wasSuccess = false
|
||||
try {
|
||||
wasSuccess = integrationService.updateLocation(it, updateLocationRequest).isSuccessful
|
||||
wasSuccess =
|
||||
integrationService.updateLocation(it, updateLocationRequest).isSuccessful
|
||||
} catch (e: Exception) {
|
||||
// Ignore failure until we are out of URLS to try!
|
||||
}
|
||||
|
@ -57,6 +63,40 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
throw IntegrationException()
|
||||
}
|
||||
|
||||
override suspend fun getZones(): Array<Entity<ZoneAttributes>> {
|
||||
val getZonesRequest = IntegrationRequest("get_zones", null)
|
||||
var zones: Array<EntityResponse<ZoneAttributes>>? = null
|
||||
for (it in getUrls()) {
|
||||
try {
|
||||
zones = integrationService.getZones(it, getZonesRequest)
|
||||
} catch (e: Exception) {
|
||||
// Ignore failure until we are out of URLS to try!
|
||||
}
|
||||
|
||||
if (zones != null) {
|
||||
return createZonesResponse(zones)
|
||||
}
|
||||
}
|
||||
|
||||
throw IntegrationException()
|
||||
}
|
||||
|
||||
override suspend fun setZoneTrackingEnabled(enabled: Boolean) {
|
||||
localStorage.putBoolean(PREF_ZONE_ENABLED, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isZoneTrackingEnabled(): Boolean {
|
||||
return localStorage.getBoolean(PREF_ZONE_ENABLED)
|
||||
}
|
||||
|
||||
override suspend fun setBackgroundTrackingEnabled(enabled: Boolean) {
|
||||
localStorage.putBoolean(PREF_BACKGROUND_ENABLED, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBackgroundTrackingEnabled(): Boolean {
|
||||
return localStorage.getBoolean(PREF_BACKGROUND_ENABLED)
|
||||
}
|
||||
|
||||
// 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>()
|
||||
|
@ -115,4 +155,22 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createZonesResponse(zones: Array<EntityResponse<ZoneAttributes>>): Array<Entity<ZoneAttributes>> {
|
||||
val retVal = ArrayList<Entity<ZoneAttributes>>()
|
||||
zones.forEach {
|
||||
retVal.add(
|
||||
Entity<ZoneAttributes>(
|
||||
it.entityId,
|
||||
it.state,
|
||||
it.attributes,
|
||||
it.lastChanged,
|
||||
it.lastUpdated,
|
||||
it.context
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return retVal.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@ import com.fasterxml.jackson.annotation.JsonInclude
|
|||
data class IntegrationRequest(
|
||||
val type: String,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
val data: Any
|
||||
val data: Any?
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.homeassistant.companion.android.data.integration
|
||||
|
||||
import io.homeassistant.companion.android.domain.integration.ZoneAttributes
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
|
@ -21,4 +22,10 @@ interface IntegrationService {
|
|||
@Url url: HttpUrl,
|
||||
@Body request: IntegrationRequest
|
||||
): Response<ResponseBody>
|
||||
|
||||
@POST
|
||||
suspend fun getZones(
|
||||
@Url url: HttpUrl,
|
||||
@Body request: IntegrationRequest
|
||||
): Array<EntityResponse<ZoneAttributes>>
|
||||
}
|
||||
|
|
|
@ -3,13 +3,17 @@ 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.Entity
|
||||
import io.homeassistant.companion.android.domain.integration.UpdateLocation
|
||||
import io.homeassistant.companion.android.domain.integration.ZoneAttributes
|
||||
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 java.util.Calendar
|
||||
import java.util.HashMap
|
||||
import kotlin.properties.Delegates
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
@ -417,5 +421,88 @@ object IntegrationRepositoryImplSpec : Spek({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("get zones") {
|
||||
beforeEachTest {
|
||||
coEvery { localStorage.getString("cloud_url") } returns "https://best.com"
|
||||
coEvery { localStorage.getString("remote_ui_url") } returns "http://better.com/"
|
||||
coEvery { authenticationRepository.getUrl() } returns URL("http://example.com")
|
||||
coEvery { localStorage.getString("webhook_id") } returns "FGHIJ"
|
||||
}
|
||||
describe("getZones") {
|
||||
val entities = EntityResponse(
|
||||
"entityId",
|
||||
"state",
|
||||
ZoneAttributes(
|
||||
false,
|
||||
0.0,
|
||||
1.1,
|
||||
2.2F,
|
||||
"fName",
|
||||
"icon"
|
||||
),
|
||||
Calendar.getInstance(),
|
||||
Calendar.getInstance(),
|
||||
HashMap()
|
||||
)
|
||||
var zones: Array<Entity<ZoneAttributes>>? = null
|
||||
beforeEachTest {
|
||||
coEvery { integrationService.getZones(any(), any()) } returns arrayOf(entities)
|
||||
runBlocking { zones = repository.getZones() }
|
||||
}
|
||||
it("should return true when webhook has a value") {
|
||||
assertThat(zones).isNotNull
|
||||
assertThat(zones!!.size).isEqualTo(1)
|
||||
assertThat(zones!![0]).isNotNull
|
||||
assertThat(zones!![0].entityId).isEqualTo(entities.entityId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("location settings") {
|
||||
describe("isZoneTrackingEnabled") {
|
||||
var isZoneTrackingEnabled by Delegates.notNull<Boolean>()
|
||||
beforeEachTest {
|
||||
coEvery { localStorage.getBoolean("zone_enabled") } returns true
|
||||
runBlocking { isZoneTrackingEnabled = repository.isZoneTrackingEnabled() }
|
||||
}
|
||||
it("should return what is stored") {
|
||||
assertThat(isZoneTrackingEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
describe("setZoneTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { repository.setZoneTrackingEnabled(true) }
|
||||
}
|
||||
it("should return what is stored") {
|
||||
coVerify {
|
||||
localStorage.putBoolean("zone_enabled", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("isBackgroundTrackingEnabled") {
|
||||
var isBackgroundTrackingEnabled by Delegates.notNull<Boolean>()
|
||||
beforeEachTest {
|
||||
coEvery { localStorage.getBoolean("background_enabled") } returns true
|
||||
runBlocking { isBackgroundTrackingEnabled = repository.isBackgroundTrackingEnabled() }
|
||||
}
|
||||
it("should return what is stored") {
|
||||
assertThat(isBackgroundTrackingEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
describe("setBackgroundTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { repository.setBackgroundTrackingEnabled(true) }
|
||||
}
|
||||
it("should return what is stored") {
|
||||
coVerify {
|
||||
localStorage.putBoolean("background_enabled", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package io.homeassistant.companion.android.domain.integration
|
||||
|
||||
import java.util.Calendar
|
||||
|
||||
data class Entity<T>(
|
||||
val entityId: String,
|
||||
val state: String,
|
||||
val attributes: T,
|
||||
val lastChanged: Calendar,
|
||||
val lastUpdated: Calendar,
|
||||
val context: Map<String, Any>
|
||||
)
|
|
@ -7,4 +7,12 @@ interface IntegrationRepository {
|
|||
suspend fun isRegistered(): Boolean
|
||||
|
||||
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||
|
||||
suspend fun getZones(): Array<Entity<ZoneAttributes>>
|
||||
|
||||
suspend fun setZoneTrackingEnabled(enabled: Boolean)
|
||||
suspend fun isZoneTrackingEnabled(): Boolean
|
||||
|
||||
suspend fun setBackgroundTrackingEnabled(enabled: Boolean)
|
||||
suspend fun isBackgroundTrackingEnabled(): Boolean
|
||||
}
|
||||
|
|
|
@ -7,4 +7,14 @@ interface IntegrationUseCase {
|
|||
suspend fun isRegistered(): Boolean
|
||||
|
||||
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||
|
||||
suspend fun getZones(): Array<Entity<ZoneAttributes>>
|
||||
|
||||
suspend fun setZoneTrackingEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isZoneTrackingEnabled(): Boolean
|
||||
|
||||
suspend fun setBackgroundTrackingEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isBackgroundTrackingEnabled(): Boolean
|
||||
}
|
||||
|
|
|
@ -16,4 +16,24 @@ class IntegrationUseCaseImpl @Inject constructor(
|
|||
override suspend fun updateLocation(updateLocation: UpdateLocation) {
|
||||
return integrationRepository.updateLocation(updateLocation)
|
||||
}
|
||||
|
||||
override suspend fun getZones(): Array<Entity<ZoneAttributes>> {
|
||||
return integrationRepository.getZones()
|
||||
}
|
||||
|
||||
override suspend fun setZoneTrackingEnabled(enabled: Boolean) {
|
||||
return integrationRepository.setZoneTrackingEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun isZoneTrackingEnabled(): Boolean {
|
||||
return integrationRepository.isZoneTrackingEnabled()
|
||||
}
|
||||
|
||||
override suspend fun setBackgroundTrackingEnabled(enabled: Boolean) {
|
||||
return integrationRepository.setBackgroundTrackingEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBackgroundTrackingEnabled(): Boolean {
|
||||
return integrationRepository.isBackgroundTrackingEnabled()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package io.homeassistant.companion.android.domain.integration
|
||||
|
||||
data class ZoneAttributes(
|
||||
val hidden: Boolean,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val radius: Float,
|
||||
val friendlyName: String,
|
||||
val icon: String
|
||||
)
|
|
@ -59,5 +59,55 @@ object IntegrationUseCaseImplSpec : Spek({
|
|||
coVerify { integrationRepository.updateLocation(location) }
|
||||
}
|
||||
}
|
||||
|
||||
describe("getZones") {
|
||||
beforeEachTest {
|
||||
runBlocking { useCase.getZones() }
|
||||
}
|
||||
|
||||
it("should call the repository") {
|
||||
coVerify { integrationRepository.getZones() }
|
||||
}
|
||||
}
|
||||
|
||||
describe("setZoneTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { useCase.setZoneTrackingEnabled(true) }
|
||||
}
|
||||
|
||||
it("should call the repository") {
|
||||
coVerify { integrationRepository.setZoneTrackingEnabled(true) }
|
||||
}
|
||||
}
|
||||
|
||||
describe("isZoneTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { useCase.isZoneTrackingEnabled() }
|
||||
}
|
||||
|
||||
it("should call the repository") {
|
||||
coVerify { integrationRepository.isZoneTrackingEnabled() }
|
||||
}
|
||||
}
|
||||
|
||||
describe("setBackgroundTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { useCase.setBackgroundTrackingEnabled(true) }
|
||||
}
|
||||
|
||||
it("should call the repository") {
|
||||
coVerify { integrationRepository.setBackgroundTrackingEnabled(true) }
|
||||
}
|
||||
}
|
||||
|
||||
describe("isBackgroundTrackingEnabled") {
|
||||
beforeEachTest {
|
||||
runBlocking { useCase.isBackgroundTrackingEnabled() }
|
||||
}
|
||||
|
||||
it("should call the repository") {
|
||||
coVerify { integrationRepository.isBackgroundTrackingEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue