Make on-boarding a more streamlined experience (#989)

* First screen working as designed.

* Error handling now working as expected.

* ktlint

* Fix minimal build.
This commit is contained in:
Justin Bassett 2020-09-30 10:37:08 -04:00 committed by GitHub
parent df627776ec
commit eeb3ab49da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 272 deletions

View file

@ -13,8 +13,8 @@ class MobileAppIntegrationPresenterImpl @Inject constructor(
) : MobileAppIntegrationPresenterBase(
view, integrationUseCase
) {
override suspend fun createRegistration(simple: Boolean): DeviceRegistration {
val registration = super.createRegistration(simple)
override suspend fun createRegistration(simple: Boolean, deviceName: String): DeviceRegistration {
val registration = super.createRegistration(simple, deviceName)
if (!simple) {
try {

View file

@ -11,23 +11,18 @@ import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ViewFlipper
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SwitchCompat
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
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.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.sensors.GeocodeSensorManager
import io.homeassistant.companion.android.sensors.LocationSensorManager
import io.homeassistant.companion.android.sensors.NetworkSensorManager
import io.homeassistant.companion.android.sensors.PhoneStateSensorManager
import io.homeassistant.companion.android.sensors.SensorWorker
import javax.inject.Inject
import kotlinx.android.synthetic.main.fragment_mobile_app_integration.*
@ -35,14 +30,12 @@ import kotlinx.android.synthetic.main.fragment_mobile_app_integration.*
class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
companion object {
private const val LOADING_VIEW = 0
private const val ERROR_VIEW = 1
private const val SETTINGS_VIEW = 2
private const val LOADING_VIEW = 1
private const val ERROR_VIEW = 2
private const val BACKGROUND_REQUEST = 99
private const val LOCATION_REQUEST_CODE = 0
private const val PHONE_REQUEST_CODE = 1
fun newInstance(): MobileAppIntegrationFragment {
return MobileAppIntegrationFragment()
@ -52,14 +45,6 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
@Inject
lateinit var presenter: MobileAppIntegrationPresenter
private lateinit var viewFlipper: ViewFlipper
private lateinit var zoneTracking: SwitchCompat
private lateinit var zoneTrackingSummary: AppCompatTextView
private lateinit var backgroundTracking: SwitchCompat
private lateinit var backgroundTrackingSummary: AppCompatTextView
private lateinit var callTracking: SwitchCompat
private lateinit var callTrackingSummary: AppCompatTextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -77,93 +62,50 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_mobile_app_integration, container, false).apply {
viewFlipper = this.findViewById(R.id.view_flipper)
findViewById<Button>(R.id.retry).setOnClickListener {
presenter.onRegistrationAttempt(false)
}
findViewById<AppCompatButton>(R.id.location_perms).apply {
setOnClickListener {
findViewById<TextInputEditText>(R.id.deviceName).setText(Build.MODEL)
findViewById<SwitchMaterial>(R.id.locationTracking)?.let {
it.isChecked = LocationSensorManager().checkPermission(context)
it.setOnCheckedChangeListener { _, isChecked ->
setLocationTracking(isChecked)
if (isChecked && !LocationSensorManager().checkPermission(requireContext())) {
this@MobileAppIntegrationFragment.requestPermissions(
LocationSensorManager().requiredPermissions(),
LOCATION_REQUEST_CODE
)
}
}
val hasLocationPermission = LocationSensorManager().checkPermission(context)
zoneTracking = findViewById<SwitchCompat>(R.id.location_zone).apply {
setOnCheckedChangeListener { _, isChecked ->
updateSensorDao(LocationSensorManager.zoneLocation.id, isChecked)
}
isEnabled = hasLocationPermission
isChecked = hasLocationPermission
}
zoneTrackingSummary = findViewById(R.id.location_zone_summary)
zoneTrackingSummary.isEnabled = hasLocationPermission
backgroundTracking = findViewById<SwitchCompat>(R.id.location_background).apply {
setOnCheckedChangeListener { _, isChecked ->
updateSensorDao(LocationSensorManager.backgroundLocation.id, isChecked)
}
isEnabled = hasLocationPermission
isChecked = hasLocationPermission && isIgnoringBatteryOptimizations()
}
backgroundTrackingSummary = findViewById(R.id.location_background_summary)
backgroundTrackingSummary.isEnabled = hasLocationPermission
// Calls tracking
findViewById<AppCompatButton>(R.id.phone_state_perms).apply {
setOnClickListener {
this@MobileAppIntegrationFragment.requestPermissions(PhoneStateSensorManager().requiredPermissions(), PHONE_REQUEST_CODE)
}
}
val hasPhoneStatePermission = PhoneStateSensorManager().checkPermission(requireContext())
callTracking = findViewById<SwitchCompat>(R.id.call_tracking).apply {
setOnCheckedChangeListener { _, isChecked ->
updateSensorDao(PhoneStateSensorManager.phoneState.id, isChecked)
}
isEnabled = hasPhoneStatePermission
isChecked = hasPhoneStatePermission
}
callTrackingSummary = findViewById(R.id.call_tracking_summary)
callTrackingSummary.isEnabled = hasPhoneStatePermission
findViewById<AppCompatButton>(R.id.finish).setOnClickListener {
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
presenter.onRegistrationAttempt(false, deviceName.text.toString())
}
findViewById<AppCompatButton>(R.id.skip).setOnClickListener {
AlertDialog.Builder(context)
.setTitle(R.string.firebase_error_title)
.setMessage(R.string.firebase_error_message)
.setPositiveButton(R.string.skip) { _, _ ->
presenter.onRegistrationAttempt(true)
findViewById<AppCompatButton>(R.id.retry).setOnClickListener {
presenter.onRegistrationAttempt(false, deviceName.text.toString())
}
.setNegativeButton(R.string.retry) { _, _ ->
presenter.onRegistrationAttempt(false)
}
.create()
.show()
}
presenter.onRegistrationAttempt(false)
}
}
override fun deviceRegistered() {
viewFlipper.displayedChild = SETTINGS_VIEW
(activity as MobileAppIntegrationListener).onIntegrationRegistrationComplete()
}
override fun showError(skippable: Boolean) {
if (skippable) {
if (skip != null)
skip.visibility = View.VISIBLE
override fun showWarning() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.firebase_error_title)
.setMessage(R.string.firebase_error_message)
.setPositiveButton(R.string.skip) { _, _ ->
presenter.onRegistrationAttempt(true, deviceName.text.toString())
}
.setNegativeButton(R.string.retry) { _, _ ->
presenter.onRegistrationAttempt(false, deviceName.text.toString())
}
.show()
}
override fun showError() {
viewFlipper.displayedChild = ERROR_VIEW
}
@ -185,46 +127,32 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_REQUEST_CODE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
zoneTracking.isEnabled = true
zoneTrackingSummary.isEnabled = true
zoneTracking.isChecked = true
updateSensorDao(LocationSensorManager.zoneLocation.id, true)
updateSensorDao(NetworkSensorManager.wifiConnection.id, true)
updateSensorDao(GeocodeSensorManager.geocodedLocation.id, true)
backgroundTracking.isEnabled = true
backgroundTrackingSummary.isEnabled = true
} else {
zoneTracking.isEnabled = false
zoneTrackingSummary.isEnabled = false
backgroundTracking.isEnabled = false
backgroundTrackingSummary.isEnabled = false
}
val hasPermission = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
locationTracking.isChecked = hasPermission
setLocationTracking(hasPermission)
requestBackgroundAccess()
}
}
if (requestCode == PHONE_REQUEST_CODE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
callTracking.isEnabled = true
callTracking.isChecked = true
callTrackingSummary.isEnabled = true
updateSensorDao(PhoneStateSensorManager.phoneState.id, true)
private fun setLocationTracking(enabled: Boolean) {
val sensorDao = AppDatabase.getInstance(requireContext()).sensorDao()
arrayOf(
LocationSensorManager.backgroundLocation,
LocationSensorManager.zoneLocation,
LocationSensorManager.singleAccurateLocation
).forEach { basicSensor ->
var sensorEntity = sensorDao.get(basicSensor.id)
if (sensorEntity != null) {
sensorEntity.enabled = enabled
sensorEntity.lastSentState = ""
sensorDao.update(sensorEntity)
} else {
callTracking.isEnabled = false
callTrackingSummary.isEnabled = false
sensorEntity = Sensor(basicSensor.id, enabled, false, "")
sensorDao.add(sensorEntity)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == BACKGROUND_REQUEST && isIgnoringBatteryOptimizations()) {
zoneTracking.isChecked = true
updateSensorDao(LocationSensorManager.backgroundLocation.id, true)
}
}
@SuppressLint("BatteryLife")
private fun requestBackgroundAccess() {
val intent: Intent
@ -243,21 +171,4 @@ class MobileAppIntegrationFragment : Fragment(), MobileAppIntegrationView {
?.isIgnoringBatteryOptimizations(activity?.packageName ?: "")
?: false
}
private fun updateSensorDao(uniqueId: String, isChecked: Boolean) {
val sensorDao = AppDatabase.getInstance(requireContext()).sensorDao()
var sensor = sensorDao.get(uniqueId)
if (sensor == null) {
sensor = Sensor(
uniqueId,
isChecked,
false,
""
)
sensorDao.add(sensor)
} else {
sensor.enabled = isChecked
sensorDao.update(sensor)
}
}
}

View file

@ -1,6 +1,6 @@
package io.homeassistant.companion.android.onboarding.integration
interface MobileAppIntegrationPresenter {
fun onRegistrationAttempt(simple: Boolean)
fun onRegistrationAttempt(simple: Boolean, deviceName: String)
fun onFinish()
}

View file

@ -1,6 +1,5 @@
package io.homeassistant.companion.android.onboarding.integration
import android.os.Build
import android.util.Log
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
@ -22,29 +21,29 @@ open class MobileAppIntegrationPresenterBase constructor(
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
internal open suspend fun createRegistration(simple: Boolean): DeviceRegistration {
internal open suspend fun createRegistration(simple: Boolean, deviceName: String): DeviceRegistration {
return DeviceRegistration(
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
Build.MODEL ?: "UNKNOWN"
deviceName
)
}
override fun onRegistrationAttempt(simple: Boolean) {
override fun onRegistrationAttempt(simple: Boolean, deviceName: String) {
view.showLoading()
mainScope.launch {
val deviceRegistration: DeviceRegistration
try {
deviceRegistration = createRegistration(simple)
deviceRegistration = createRegistration(simple, deviceName)
} catch (e: Exception) {
Log.e(TAG, "Unable to create registration.", e)
view.showError(true)
view.showWarning()
return@launch
}
try {
integrationUseCase.registerDevice(deviceRegistration)
} catch (e: Exception) {
Log.e(TAG, "Unable to register with Home Assistant", e)
view.showError(false)
view.showError()
return@launch
}
view.deviceRegistered()

View file

@ -6,5 +6,7 @@ interface MobileAppIntegrationView {
fun showLoading()
fun showError(skippable: Boolean = false)
fun showWarning()
fun showError()
}

View file

@ -1,9 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/view_flipper"
android:id="@+id/viewFlipper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.HomeAssistant.Headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/connect_to_home_assistant"
android:textAlignment="center" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:hint="@string/device_name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/deviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/locationTracking"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/enable_location_tracking"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/location_background_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
android:text="@string/enable_location_tracking_description" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="@dimen/activity_margin"
android:gravity="end|bottom">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/finish"
style="@style/Widget.HomeAssistant.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/finish" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
@ -66,119 +128,6 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/skip"
style="@style/Widget.HomeAssistant.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/skip" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.HomeAssistant.Headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/connected_to_home_assistant"
android:textAlignment="center" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.HomeAssistant.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/permission_explanation" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/location_perms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/grant_permission" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/location_zone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/pref_location_zone_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/location_zone_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
android:text="@string/sensor_description_location_zone" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/location_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/pref_location_background_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/location_background_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
android:text="@string/sensor_description_location_background" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.HomeAssistant.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/permission_explanation_calls" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/phone_state_perms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/grant_permission" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/call_tracking"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:text="@string/pref_call_tracking_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/call_tracking_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
android:text="@string/pref_call_tracking_summary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="end|bottom">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/finish"
style="@style/Widget.HomeAssistant.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/finish" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</ViewFlipper>

View file

@ -312,4 +312,7 @@ like to connect to:</string>
<string name="widget_text_hint_service_service">Service</string>
<string name="widget_text_size_default">30</string>
<string name="widget_text_size_label">Widget text size:</string>
<string name="connect_to_home_assistant">Connect to Home Assistant</string>
<string name="enable_location_tracking">Enable Location Tracking</string>
<string name="enable_location_tracking_description">Enabling this sensor will allow the Home Assistant application to track you location and report it back to your instance of Home Assistant. Ensure that you enable background access to location, otherwise we cannot enable location tracking.</string>
</resources>

View file

@ -22,12 +22,20 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
val backgroundLocation = SensorManager.BasicSensor(
"location_background",
"",
R.string.basic_sensor_name_location_background
R.string.basic_sensor_name_location_background,
R.string.sensor_description_location_background
)
val zoneLocation = SensorManager.BasicSensor(
"zone_background",
"",
R.string.basic_sensor_name_location_zone
R.string.basic_sensor_name_location_zone,
R.string.sensor_description_location_zone
)
val singleAccurateLocation = SensorManager.BasicSensor(
"accurate_location",
"",
R.string.basic_sensor_name_location_accurate,
R.string.sensor_description_location_accurate
)
internal const val TAG = "LocBroadcastReceiver"
}

View file

@ -41,7 +41,7 @@ object MobileAppIntegrationPresenterImplSpec : Spek({
}
describe("register") {
beforeEachTest {
presenter.onRegistrationAttempt(true)
presenter.onRegistrationAttempt(true, Build.MODEL ?: "UNKNOWN")
}
it("should register successfully") {
coVerify {
@ -59,7 +59,7 @@ object MobileAppIntegrationPresenterImplSpec : Spek({
}
describe("register") {
beforeEachTest {
presenter.onRegistrationAttempt(false)
presenter.onRegistrationAttempt(false, Build.MODEL ?: "UNKNOWN")
}
it("should fail") {
coVerifyAll {