Fix Biometric Unlock & Enhancement (#625)

* Fix Biometric Unlock
* Add Prompt Unlock function directly in WebViewActivity
* Added session timeout in app settings
* App is locked when resumed (after session timed out)
* When the application is locked, the screen is blurred (no quite obnoxious splash screen to look at), which allows the HA frontend to be loaded before or during unlocking
* No button needs to be touched for unlock
* When the unlock prompt is displayed, the back button close the app
* Fix issues #555 and #587
This commit is contained in:
Neonicus 2020-07-05 16:09:17 +02:00 committed by GitHub
parent 90ccc7e2cd
commit 756556046a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 357 additions and 261 deletions

View file

@ -96,6 +96,8 @@ dependencies {
implementation(project(":common"))
implementation(project(":domain"))
implementation(Config.Dependency.Misc.blurView)
implementation(Config.Dependency.Kotlin.core)
implementation(Config.Dependency.Kotlin.coroutines)
implementation(Config.Dependency.Kotlin.coroutinesAndroid)

View file

@ -96,16 +96,17 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".onboarding.OnboardingActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity android:name=".lock.LockActivity"/>
<activity
android:name=".webview.WebViewActivity"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" />
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".webview.WebViewActivity" />

View file

@ -3,7 +3,6 @@ package io.homeassistant.companion.android
import dagger.Component
import io.homeassistant.companion.android.common.dagger.AppComponent
import io.homeassistant.companion.android.launch.LaunchActivity
import io.homeassistant.companion.android.lock.LockActivity
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
import io.homeassistant.companion.android.onboarding.discovery.DiscoveryFragment
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationFragment
@ -35,7 +34,5 @@ interface PresenterComponent {
fun inject(activity: WebViewActivity)
fun inject(activity: LockActivity)
fun inject(dialog: SsidDialogFragment)
}

View file

@ -6,9 +6,6 @@ import dagger.Provides
import io.homeassistant.companion.android.launch.LaunchPresenter
import io.homeassistant.companion.android.launch.LaunchPresenterImpl
import io.homeassistant.companion.android.launch.LaunchView
import io.homeassistant.companion.android.lock.LockPresenter
import io.homeassistant.companion.android.lock.LockPresenterImpl
import io.homeassistant.companion.android.lock.LockView
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenter
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenterImpl
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationView
@ -42,7 +39,6 @@ class PresenterModule {
private lateinit var settingsView: SettingsView
private lateinit var shortcutsView: ShortcutsView
private lateinit var webView: WebView
private lateinit var lockView: LockView
constructor(launchView: LaunchView) {
this.launchView = launchView
@ -76,10 +72,6 @@ class PresenterModule {
this.webView = webView
}
constructor(lockView: LockView) {
this.lockView = lockView
}
@Provides
fun provideLaunchView() = launchView
@ -104,9 +96,6 @@ class PresenterModule {
@Provides
fun provideWebView() = webView
@Provides
fun provideLockView() = lockView
@Module
interface Declaration {
@ -133,8 +122,5 @@ class PresenterModule {
@Binds
fun bindWebViewPresenterImpl(presenter: WebViewPresenterImpl): WebViewPresenter
@Binds
fun bindLockPresenter(presenter: LockPresenterImpl): LockPresenter
}
}

View file

@ -0,0 +1,50 @@
package io.homeassistant.companion.android.authenticator
import android.content.Context
import android.util.Log
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import io.homeassistant.companion.android.R
class Authenticator(context: Context, fragmentActivity: FragmentActivity, callback: (Int) -> Unit) {
companion object {
const val CANCELED = 2
const val SUCCESS = 1
const val ERROR = 0
}
var title = fragmentActivity.resources.getString(R.string.biometric_title)
private val executor = ContextCompat.getMainExecutor(context)
private val biometricPrompt = BiometricPrompt(fragmentActivity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d("Unlock", "onAuthenticationError -> $errorCode :: $errString")
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED)
callback(CANCELED)
else
callback(ERROR)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
callback(ERROR)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
callback(SUCCESS)
}
})
fun authenticate() {
val promptDialog = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setDeviceCredentialAllowed(true)
.build()
biometricPrompt.authenticate(promptDialog)
}
}

View file

@ -9,7 +9,6 @@ import com.lokalise.sdk.menu_inflater.LokaliseMenuInflater
import io.homeassistant.companion.android.DaggerPresenterComponent
import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.lock.LockActivity
import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.webview.WebViewActivity
import javax.inject.Inject
@ -31,12 +30,8 @@ class LaunchActivity : AppCompatActivity(), LaunchView {
presenter.onViewReady()
}
override fun displayLockView() {
startActivity(LockActivity.newInstance(this))
finish()
}
override fun displayWebview() {
presenter.setSessionExpireMillis(0)
startActivity(WebViewActivity.newInstance(this))
finish()
}

View file

@ -4,5 +4,7 @@ interface LaunchPresenter {
fun onViewReady()
fun setSessionExpireMillis(value: Long)
fun onFinish()
}

View file

@ -32,16 +32,19 @@ class LaunchPresenterImpl @Inject constructor(
val sessionValid = authenticationUseCase.getSessionState() == SessionState.CONNECTED
if (sessionValid && integrationUseCase.isRegistered()) {
resyncRegistration()
if (authenticationUseCase.isLockEnabled())
view.displayLockView()
else
view.displayWebview()
view.displayWebview()
} else {
view.displayOnBoarding(sessionValid)
}
}
}
override fun setSessionExpireMillis(value: Long) {
mainScope.launch {
integrationUseCase.setSessionExpireMillis(value)
}
}
override fun onFinish() {
mainScope.cancel()
}

View file

@ -1,9 +1,6 @@
package io.homeassistant.companion.android.launch
interface LaunchView {
fun displayLockView()
fun displayWebview()
fun displayOnBoarding(sessionConnected: Boolean)

View file

@ -1,76 +0,0 @@
package io.homeassistant.companion.android.lock
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
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.webview.WebViewActivity
import javax.inject.Inject
class LockActivity : AppCompatActivity(), LockView {
companion object {
fun newInstance(context: Context): Intent {
return Intent(context, LockActivity::class.java)
}
}
@Inject
lateinit var presenter: LockPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock)
DaggerPresenterComponent
.builder()
.appComponent((application as GraphComponentAccessor).appComponent)
.presenterModule(PresenterModule(this))
.build()
.inject(this)
if (presenter.isLockEnabled()) {
val button = findViewById<ImageView>(R.id.unlockButton)
button.setOnClickListener {
if (BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
promptForUnlock()
}
}
}
}
private fun promptForUnlock() {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
presenter.onViewReady()
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(this.resources.getString(R.string.biometric_title))
.setSubtitle(this.resources.getString(R.string.biometric_message))
.setDeviceCredentialAllowed(true)
.build()
biometricPrompt.authenticate(promptInfo)
}
override fun displayWebview() {
startActivity(WebViewActivity.newInstance(this))
finish()
}
}

View file

@ -1,7 +0,0 @@
package io.homeassistant.companion.android.lock
interface LockPresenter {
fun isLockEnabled(): Boolean
fun onViewReady()
}

View file

@ -1,29 +0,0 @@
package io.homeassistant.companion.android.lock
import io.homeassistant.companion.android.domain.authentication.AuthenticationUseCase
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class LockPresenterImpl @Inject constructor(
private val view: LockView,
private val authenticationUseCase: AuthenticationUseCase
) : LockPresenter {
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
override fun isLockEnabled(): Boolean {
return runBlocking {
authenticationUseCase.isLockEnabled()
}
}
override fun onViewReady() {
mainScope.launch {
view.displayWebview()
}
}
}

View file

@ -1,5 +0,0 @@
package io.homeassistant.companion.android.lock
interface LockView {
fun displayWebview()
}

View file

@ -28,6 +28,7 @@ class SettingsActivity : AppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager

View file

@ -4,16 +4,17 @@ import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.app.ActivityCompat.finishAffinity
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import eightbitlab.com.blurview.BlurView
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.authenticator.Authenticator
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.settings.shortcuts.ShortcutsFragment
import io.homeassistant.companion.android.settings.ssid.SsidDialogFragment
@ -32,6 +33,10 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
@Inject
lateinit var presenter: SettingsPresenter
private lateinit var authenticator: Authenticator
private lateinit var blurView: BlurView
private var unlocked = false
private var setLock = false
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
DaggerPresenterComponent
@ -41,6 +46,16 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
.build()
.inject(this)
blurView = requireActivity().findViewById(R.id.blurView)
blurView.setupWith(requireActivity().findViewById(R.id.content))
.setOverlayColor(resources.getColor(R.color.colorOnPrimary))
if (!presenter.isLockEnabled())
blurView.setBlurEnabled(false)
authenticator = Authenticator(requireContext(), requireActivity(), ::authenticationResult)
preferenceManager.preferenceDataStore = presenter.getPreferenceDataStore()
setPreferencesFromResource(R.xml.preferences, rootKey)
@ -63,9 +78,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
isValid = true
else {
isValid = true
if (BiometricManager.from(requireActivity()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS)
promptForUnlock()
else {
if (BiometricManager.from(requireActivity()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
setLock = true
authenticator.title = getString(R.string.biometric_set_title)
authenticator.authenticate()
} else {
isValid = false
AlertDialog.Builder(requireActivity())
.setTitle(R.string.set_lock_title)
@ -160,30 +177,36 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
}
}
private fun promptForUnlock() {
val executor = ContextCompat.getMainExecutor(requireActivity())
override fun onResume() {
super.onResume()
if (presenter.isLockEnabled() && !unlocked)
if (System.currentTimeMillis() > presenter.getSessionExpireMillis()) {
blurView.setBlurEnabled(true)
setLock = false
authenticator.title = getString(R.string.biometric_title)
authenticator.authenticate()
} else blurView.setBlurEnabled(false)
}
override fun onPause() {
presenter.setSessionExpireMillis((System.currentTimeMillis() + (presenter.sessionTimeOut() * 1000)))
unlocked = false
super.onPause()
}
private fun authenticationResult(result: Int) {
val switchLock = findPreference<SwitchPreference>("app_lock")
val biometricPrompt = BiometricPrompt(requireActivity(), executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
switchLock?.isChecked = false
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
switchLock?.isChecked = false
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(requireActivity().resources.getString(R.string.biometric_title))
.setSubtitle(requireActivity().resources.getString(R.string.biometric_message))
.setDeviceCredentialAllowed(true)
.build()
biometricPrompt.authenticate(promptInfo)
if (result == Authenticator.SUCCESS) {
if (!setLock) {
unlocked = true
blurView.setBlurEnabled(false)
} else switchLock?.isChecked = true
} else {
switchLock?.isChecked = false
if (result == Authenticator.CANCELED) {
if (!setLock)
finishAffinity(requireActivity())
} else authenticator.authenticate()
}
}
}

View file

@ -8,4 +8,9 @@ interface SettingsPresenter {
fun onCreate()
fun onFinish()
fun getPanels(): Array<Panel>
fun isLockEnabled(): Boolean
fun sessionTimeOut(): Int
fun setSessionExpireMillis(value: Long)
fun getSessionExpireMillis(): Long
}

View file

@ -59,6 +59,7 @@ class SettingsPresenterImpl @Inject constructor(
"connection_internal" -> (urlUseCase.getUrl(true) ?: "").toString()
"connection_external" -> (urlUseCase.getUrl(false) ?: "").toString()
"registration_name" -> integrationUseCase.getRegistration().deviceName
"session_timeout" -> integrationUseCase.getSessionTimeOut().toString()
else -> throw IllegalArgumentException("No string found by this key: $key")
}
}
@ -69,6 +70,7 @@ class SettingsPresenterImpl @Inject constructor(
when (key) {
"connection_internal" -> urlUseCase.saveUrl(value ?: "", true)
"connection_external" -> urlUseCase.saveUrl(value ?: "", false)
"session_timeout" -> integrationUseCase.sessionTimeOut(value.toString().toInt())
"registration_name" -> {
try {
integrationUseCase.updateRegistration(deviceName = value!!)
@ -102,6 +104,24 @@ class SettingsPresenterImpl @Inject constructor(
}
}
override fun getInt(key: String, defValue: Int): Int {
return runBlocking {
when (key) {
"session_timeout" -> integrationUseCase.getSessionTimeOut()
else -> throw IllegalArgumentException("No int found by this key: $key")
}
}
}
override fun putInt(key: String, value: Int) {
mainScope.launch {
when (key) {
"session_timeout" -> integrationUseCase.sessionTimeOut(value)
else -> throw IllegalArgumentException("No int found by this key: $key")
}
}
}
override fun getPreferenceDataStore(): PreferenceDataStore {
return this
}
@ -136,4 +156,28 @@ class SettingsPresenterImpl @Inject constructor(
panels
}
}
override fun isLockEnabled(): Boolean {
return runBlocking {
authenticationUseCase.isLockEnabled()
}
}
override fun sessionTimeOut(): Int {
return runBlocking {
integrationUseCase.getSessionTimeOut()
}
}
override fun setSessionExpireMillis(value: Long) {
mainScope.launch {
integrationUseCase.setSessionExpireMillis(value)
}
}
override fun getSessionExpireMillis(): Long {
return runBlocking {
integrationUseCase.getSessionExpireMillis()
}
}
}

View file

@ -35,10 +35,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.lokalise.sdk.LokaliseContextWrapper
import com.lokalise.sdk.menu_inflater.LokaliseMenuInflater
import eightbitlab.com.blurview.RenderScriptBlur
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.authenticator.Authenticator
import io.homeassistant.companion.android.background.LocationBroadcastReceiver
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.onboarding.OnboardingActivity
@ -46,6 +48,7 @@ import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.util.PermissionManager
import io.homeassistant.companion.android.util.isStarted
import javax.inject.Inject
import kotlinx.android.synthetic.main.activity_webview.*
import org.json.JSONObject
class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.webview.WebView {
@ -70,12 +73,14 @@ class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.
private lateinit var loadedUrl: String
private lateinit var decor: FrameLayout
private lateinit var myCustomView: View
private lateinit var authenticator: Authenticator
private var isConnected = false
private var isShowingError = false
private var alertDialog: AlertDialog? = null
private var isVideoFullScreen = false
private var videoHeight = 0
private var unlocked = false
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
@ -97,6 +102,16 @@ class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.
WebView.setWebContentsDebuggingEnabled(true)
}
blurView.setupWith(root)
.setBlurAlgorithm(RenderScriptBlur(this))
.setBlurRadius(5f)
.setHasFixedTransformationMatrix(false)
if (!presenter.isLockEnabled())
blurView.setBlurEnabled(false)
authenticator = Authenticator(this, this, ::authenticationResult)
decor = window.decorView as FrameLayout
webView = findViewById(R.id.webview)
@ -307,11 +322,27 @@ class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.
}
}
private fun authenticationResult(result: Int) {
if (result == Authenticator.SUCCESS) {
unlocked = true
blurView.setBlurEnabled(false)
} else if (result == Authenticator.CANCELED)
finishAffinity()
else authenticator.authenticate()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
if (presenter.isLockEnabled() && !unlocked)
if ((System.currentTimeMillis() > presenter.getSessionExpireMillis())) {
blurView.setBlurEnabled(true)
authenticator.authenticate()
} else blurView.setBlurEnabled(false)
presenter.onViewReady(intent.getStringExtra(EXTRA_PATH))
intent.removeExtra(EXTRA_PATH)
if (presenter.isFullScreen())
hideSystemUI()
else
@ -320,7 +351,6 @@ class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.
}
private fun hideSystemUI() {
if (isCutout())
decor.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
@ -368,6 +398,8 @@ class WebViewActivity : AppCompatActivity(), io.homeassistant.companion.android.
override fun onUserLeaveHint() {
super.onUserLeaveHint()
presenter.setSessionExpireMillis((System.currentTimeMillis() + (presenter.sessionTimeOut() * 1000)))
unlocked = false
videoHeight = decor.height
var bounds = Rect(0, 0, 1920, 1080)
if (isVideoFullScreen) {

View file

@ -16,5 +16,12 @@ interface WebViewPresenter {
fun isFullScreen(): Boolean
fun isLockEnabled(): Boolean
fun sessionTimeOut(): Int
fun setSessionExpireMillis(value: Long)
fun getSessionExpireMillis(): Long
fun onFinish()
}

View file

@ -116,6 +116,30 @@ class WebViewPresenterImpl @Inject constructor(
}
}
override fun isLockEnabled(): Boolean {
return runBlocking {
authenticationUseCase.isLockEnabled()
}
}
override fun sessionTimeOut(): Int {
return runBlocking {
integrationUseCase.getSessionTimeOut()
}
}
override fun setSessionExpireMillis(value: Long) {
mainScope.launch {
integrationUseCase.setSessionExpireMillis(value)
}
}
override fun getSessionExpireMillis(): Long {
return runBlocking {
integrationUseCase.getSessionExpireMillis()
}
}
override fun onFinish() {
mainScope.cancel()
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/colorAccent"
android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/lockScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/lockTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:text="@string/lockscreen_title"
android:textAllCaps="true"
android:textSize="36sp" />
<TextView
android:id="@+id/lockMess"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/lockscreen_message"
android:textSize="30sp" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<ImageButton
android:id="@+id/unlockButton"
android:layout_width="185dp"
android:layout_height="184dp"
android:adjustViewBounds="true"
android:duplicateParentState="false"
android:src="@drawable/lock_icon" />
</LinearLayout>
</LinearLayout>

View file

@ -1,17 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
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_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root2"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.HomeAssistant.ActionBar"
android:elevation="4dp" />
<FrameLayout
android:id="@+id/content"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.HomeAssistant.ActionBar"
android:elevation="4dp" />
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<eightbitlab.com.blurview.BlurView
android:id="@+id/blurView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
android:layout_height="match_parent">
</eightbitlab.com.blurview.BlurView>
</RelativeLayout>

View file

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webview"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<eightbitlab.com.blurview.BlurView
android:id="@+id/blurView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</eightbitlab.com.blurview.BlurView>
</RelativeLayout>

View file

@ -76,8 +76,9 @@ like to connect to:</string>
<string name="pref_connection_ssids_empty">Configure your WiFi SSID(s)</string>
<string name="lockscreen_title">App Locked !</string>
<string name="lockscreen_message">Tap to unlock :</string>
<string name="biometric_title">Home Assistant need to be unlock</string>
<string name="biometric_title">Home Assistant is locked</string>
<string name="biometric_message">Unlock using your biometric or screenlock credential</string>
<string name="biometric_set_title">Confirm to continue</string>
<string name="set_lock_title">App Locking Error</string>
<string name="set_lock_message">No biometric sensor or screenlock credential available</string>
<string name="lock_title">Lock app</string>
@ -101,6 +102,7 @@ like to connect to:</string>
<string name="states">Overview</string>
<string name="shortcuts">Shortcuts</string>
<string name="shortcuts_summary">Add shortcuts to launcher</string>
<string name="session_timeout_title">Session TimeOut (in seconds)</string>
<string name="rate_limit_notification.title">Notifications Rate Limited</string>
<string name="rate_limit_notification.body">You have now sent more than %s notifications today. You will not receive new notifications until midnight UTC.</string>
<string name="webview_error_description">Encountered error :</string>

View file

@ -52,6 +52,11 @@
android:icon="@drawable/ic_lock"
android:title="@string/lock_title"
android:summary="@string/lock_summary"/>
<EditTextPreference
android:key="session_timeout"
android:icon="@drawable/ic_timeout"
android:title="@string/session_timeout_title"
app:useSimpleSummaryProvider="true"/>
<Preference
android:key="shortcuts"
android:icon="@drawable/ic_plus"

View file

@ -89,24 +89,6 @@ object LaunchPresenterImplSpec : Spek({
}
}
describe("connected state") {
beforeEachTest {
coEvery { authenticationUseCase.getSessionState() } returns SessionState.CONNECTED
coEvery { integrationUseCase.isRegistered() } returns true
coEvery { authenticationUseCase.isLockEnabled() } returns true
}
describe("on view ready") {
beforeEachTest {
presenter.onViewReady()
}
it("should display the lockview") {
verify { view.displayLockView() }
}
}
}
describe("connected state but not integrated") {
beforeEachTest {
coEvery { authenticationUseCase.getSessionState() } returns SessionState.CONNECTED

View file

@ -93,6 +93,7 @@ object Config {
const val threeTenBp = "org.threeten:threetenbp:1.4.0"
const val threeTenAbp = "com.jakewharton.threetenabp:threetenabp:1.2.1"
const val javaxInject = "javax.inject:javax.inject:1"
const val blurView = "com.eightbitlab:blurview:1.6.3"
}
}

View file

@ -29,6 +29,22 @@ class LocalStorageImpl(private val sharedPreferences: SharedPreferences) : Local
}
}
override suspend fun putInt(key: String, value: Int?) {
if (value == null) {
sharedPreferences.edit().remove(key).apply()
} else {
sharedPreferences.edit().putInt(key, value).apply()
}
}
override suspend fun getInt(key: String): Int? {
return if (sharedPreferences.contains(key)) {
sharedPreferences.getInt(key, 0)
} else {
null
}
}
override suspend fun putBoolean(key: String, value: Boolean) {
sharedPreferences.edit().putBoolean(key, value).apply()
}

View file

@ -10,6 +10,10 @@ interface LocalStorage {
suspend fun getLong(key: String): Long?
suspend fun putInt(key: String, value: Int?)
suspend fun getInt(key: String): Int?
suspend fun putBoolean(key: String, value: Boolean)
suspend fun getBoolean(key: String): Boolean

View file

@ -50,6 +50,8 @@ class IntegrationRepositoryImpl @Inject constructor(
private const val PREF_ZONE_ENABLED = "zone_enabled"
private const val PREF_BACKGROUND_ENABLED = "background_enabled"
private const val PREF_FULLSCREEN_ENABLED = "fullscreen_enabled"
private const val PREF_SESSION_TIMEOUT = "session_timeout"
private const val PREF_SESSION_EXPIRE = "session_expire"
private const val PREF_SENSORS_REGISTERED = "sensors_registered"
}
@ -244,6 +246,22 @@ class IntegrationRepositoryImpl @Inject constructor(
return localStorage.getBoolean(PREF_FULLSCREEN_ENABLED)
}
override suspend fun sessionTimeOut(value: Int) {
localStorage.putInt(PREF_SESSION_TIMEOUT, value)
}
override suspend fun getSessionTimeOut(): Int {
return localStorage.getInt(PREF_SESSION_TIMEOUT) ?: 0
}
override suspend fun setSessionExpireMillis(value: Long) {
localStorage.putLong(PREF_SESSION_EXPIRE, value)
}
override suspend fun getSessionExpireMillis(): Long {
return localStorage.getLong(PREF_SESSION_EXPIRE) ?: 0
}
override suspend fun getThemeColor(): String {
val getConfigRequest =
IntegrationRequest(

View file

@ -21,6 +21,12 @@ interface IntegrationRepository {
suspend fun setFullScreenEnabled(enabled: Boolean)
suspend fun isFullScreenEnabled(): Boolean
suspend fun sessionTimeOut(value: Int)
suspend fun getSessionTimeOut(): Int
suspend fun setSessionExpireMillis(value: Long)
suspend fun getSessionExpireMillis(): Long
suspend fun getThemeColor(): String
suspend fun getPanels(): Array<Panel>

View file

@ -34,6 +34,12 @@ interface IntegrationUseCase {
suspend fun setFullScreenEnabled(enabled: Boolean)
suspend fun isFullScreenEnabled(): Boolean
suspend fun sessionTimeOut(value: Int)
suspend fun getSessionTimeOut(): Int
suspend fun setSessionExpireMillis(value: Long)
suspend fun getSessionExpireMillis(): Long
suspend fun getServices(): Array<Service>
suspend fun getEntities(): Array<Entity<Any>>

View file

@ -76,6 +76,22 @@ class IntegrationUseCaseImpl @Inject constructor(
return integrationRepository.isFullScreenEnabled()
}
override suspend fun sessionTimeOut(value: Int) {
return integrationRepository.sessionTimeOut(value)
}
override suspend fun getSessionTimeOut(): Int {
return integrationRepository.getSessionTimeOut()
}
override suspend fun setSessionExpireMillis(value: Long) {
return integrationRepository.setSessionExpireMillis(value)
}
override suspend fun getSessionExpireMillis(): Long {
return integrationRepository.getSessionExpireMillis()
}
override suspend fun getServices(): Array<Service> {
return integrationRepository.getServices()
}