mirror of
https://github.com/home-assistant/android
synced 2024-11-05 18:28:42 +00:00
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:
parent
90ccc7e2cd
commit
756556046a
35 changed files with 357 additions and 261 deletions
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -4,5 +4,7 @@ interface LaunchPresenter {
|
|||
|
||||
fun onViewReady()
|
||||
|
||||
fun setSessionExpireMillis(value: Long)
|
||||
|
||||
fun onFinish()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package io.homeassistant.companion.android.launch
|
||||
|
||||
interface LaunchView {
|
||||
|
||||
fun displayLockView()
|
||||
|
||||
fun displayWebview()
|
||||
|
||||
fun displayOnBoarding(sessionConnected: Boolean)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package io.homeassistant.companion.android.lock
|
||||
|
||||
interface LockPresenter {
|
||||
fun isLockEnabled(): Boolean
|
||||
|
||||
fun onViewReady()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package io.homeassistant.companion.android.lock
|
||||
|
||||
interface LockView {
|
||||
fun displayWebview()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -16,5 +16,12 @@ interface WebViewPresenter {
|
|||
|
||||
fun isFullScreen(): Boolean
|
||||
|
||||
fun isLockEnabled(): Boolean
|
||||
|
||||
fun sessionTimeOut(): Int
|
||||
|
||||
fun setSessionExpireMillis(value: Long)
|
||||
fun getSessionExpireMillis(): Long
|
||||
|
||||
fun onFinish()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_timeout.xml
Normal file
9
app/src/main/res/drawable/ic_timeout.xml
Normal 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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue