diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 361800623c..32594dbc24 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -49,6 +49,7 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.pin.PinCodeStore +import im.vector.riotx.features.pin.PinLocker import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences @@ -84,7 +85,7 @@ class VectorApplication : @Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var rxConfig: RxConfig @Inject lateinit var popupAlertManager: PopupAlertManager - @Inject lateinit var pinCodeStore: PinCodeStore + @Inject lateinit var pinLocker: PinLocker lateinit var vectorComponent: VectorComponent @@ -156,6 +157,7 @@ class VectorApplication : } }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) + ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) // This should be done as early as possible // initKnownEmojiHashSet(appContext) } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 2838a42169..2d51a6b6cb 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -54,6 +54,7 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.permalink.PermalinkHandlerActivity +import im.vector.riotx.features.pin.PinLocker import im.vector.riotx.features.qrcode.QrCodeScannerActivity import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter @@ -101,6 +102,7 @@ interface ScreenComponent { fun bugReporter(): BugReporter fun rageShake(): RageShake fun navigator(): Navigator + fun pinLocker(): PinLocker fun errorFormatter(): ErrorFormatter fun uiStateRepository(): UiStateRepository fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 66d0a70343..fe627a0d04 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -49,6 +49,7 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.pin.PinCodeStore +import im.vector.riotx.features.pin.PinLocker import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger @@ -141,6 +142,8 @@ interface VectorComponent { fun reAuthHelper(): ReAuthHelper + fun pinLocker(): PinLocker + fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager @Component.Factory diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 355211643c..d511b6da9b 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -16,7 +16,9 @@ package im.vector.riotx.core.platform +import android.app.Activity import android.content.Context +import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable @@ -58,6 +60,7 @@ import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.observeNotNull import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.core.utils.toast import im.vector.riotx.features.MainActivity @@ -65,6 +68,10 @@ import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.consent.ConsentNotGivenHelper import im.vector.riotx.features.navigation.Navigator +import im.vector.riotx.features.pin.PinActivity +import im.vector.riotx.features.pin.PinLocker +import im.vector.riotx.features.pin.PinMode +import im.vector.riotx.features.pin.UnlockedActivity import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -116,6 +123,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter + private lateinit var pinLocker: PinLocker lateinit var rageShake: RageShake lateinit var navigator: Navigator @@ -181,6 +189,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { viewModelFactory = screenComponent.viewModelFactory() configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) bugReporter = screenComponent.bugReporter() + pinLocker = screenComponent.pinLocker() // Shake detector rageShake = screenComponent.rageShake() navigator = screenComponent.navigator() @@ -193,7 +202,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { finish() } }) - + pinLocker.getLiveState().observeNotNull(this) { + if(this@VectorBaseActivity !is UnlockedActivity && it == PinLocker.State.LOCKED){ + navigator.openPinCode(this, PinMode.AUTH) + } + } sessionListener = vectorComponent.sessionListener() sessionListener.globalErrorLiveData.observeEvent(this) { handleGlobalError(it) @@ -285,6 +298,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { uiDisposables.dispose() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PinActivity.PIN_REQUEST_CODE) { + when (resultCode) { + Activity.RESULT_OK -> { + pinLocker.unlock() + } + PinActivity.PIN_RESULT_CODE_FORGOT -> { + pinLocker.block() + } + else -> { + pinLocker.block() + moveTaskToBack(true) + } + } + } + } + override fun onResume() { super.onResume() Timber.i("onResume Activity ${this.javaClass.simpleName}") diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index cda7695a12..d9b96ad01b 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -280,6 +280,11 @@ class DefaultNavigator @Inject constructor( fragment.startActivityForResult(intent, requestCode) } + override fun openPinCode(activity: Activity, pinMode: PinMode, requestCode: Int) { + val intent = PinActivity.newIntent(activity, PinArgs(pinMode)) + activity.startActivityForResult(intent, requestCode) + } + override fun openMediaViewer(activity: Activity, roomId: String, mediaData: AttachmentData, diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index e5dea85040..b5733285db 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -82,6 +82,8 @@ interface Navigator { fun openPinCode(fragment: Fragment, pinMode: PinMode, requestCode: Int = PinActivity.PIN_REQUEST_CODE) + fun openPinCode(activity: Activity, pinMode: PinMode, requestCode: Int = PinActivity.PIN_REQUEST_CODE) + fun openTerms(fragment: Fragment, serviceType: TermsService.ServiceType, baseUrl: String, diff --git a/vector/src/main/java/im/vector/riotx/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/riotx/features/pin/PinActivity.kt index 8c2d9cb130..ceb15b5862 100644 --- a/vector/src/main/java/im/vector/riotx/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/pin/PinActivity.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity -class PinActivity : VectorBaseActivity(), ToolbarConfigurable { +class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/riotx/features/pin/PinFragment.kt index 4765d2f799..43de8dcb5c 100644 --- a/vector/src/main/java/im/vector/riotx/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/pin/PinFragment.kt @@ -141,6 +141,7 @@ class PinFragment @Inject constructor( override fun onFingerprintSuccessful() { Toast.makeText(requireContext(), "Pin successful", Toast.LENGTH_LONG).show() + vectorBaseActivity.setResult(Activity.RESULT_OK) vectorBaseActivity.finish() } @@ -149,6 +150,7 @@ class PinFragment @Inject constructor( override fun onCodeInputSuccessful() { Toast.makeText(requireContext(), "Pin successful", Toast.LENGTH_LONG).show() + vectorBaseActivity.setResult(Activity.RESULT_OK) vectorBaseActivity.finish() } }) diff --git a/vector/src/main/java/im/vector/riotx/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/riotx/features/pin/PinLocker.kt new file mode 100644 index 0000000000..51d22147fe --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/pin/PinLocker.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.pin + +import android.os.SystemClock +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.OnLifecycleEvent +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +private const val PERIOD_OF_GRACE_IN_MS = 0 * 60 * 1000L + +/** + * This class is responsible for keeping the status of locking + * It automatically locks when entering background/foreground with a grace period. + * You can force to unlock with unlock method, use it whenever the pin code has been validated. + */ + +@Singleton +class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : LifecycleObserver { + + enum class State { + // App is locked, can be unlock + LOCKED, + + // App is blocked and can't be unlocked as long as the app is in foreground + BLOCKED, + + // is unlocked, the app can be used + UNLOCKED + } + + private val liveState = MutableLiveData() + + private var isBlocked = false + private var shouldBeLocked = true + private var entersBackgroundTs = 0L + + fun getLiveState(): LiveData { + return liveState + } + + private fun computeState() { + GlobalScope.launch { + val state = if (isBlocked) { + State.BLOCKED + } else if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { + State.LOCKED + } else { + State.UNLOCKED + } + if (liveState.value != state) { + liveState.postValue(state) + } + } + } + + fun unlock() { + Timber.v("Unlock app") + shouldBeLocked = false + computeState() + } + + fun block() { + Timber.v("Block app") + isBlocked = true + computeState() + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun entersForeground() { + val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs + shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= PERIOD_OF_GRACE_IN_MS + Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background") + computeState() + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun entersBackground() { + isBlocked = false + entersBackgroundTs = SystemClock.elapsedRealtime() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/pin/UnlockedActivity.kt b/vector/src/main/java/im/vector/riotx/features/pin/UnlockedActivity.kt new file mode 100644 index 0000000000..30f0755954 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/pin/UnlockedActivity.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.pin + +interface UnlockedActivity diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index b1fca9841b..71a6cd4a23 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -56,15 +56,16 @@ import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.pin.PinActivity import im.vector.riotx.features.pin.PinCodeStore +import im.vector.riotx.features.pin.PinLocker import im.vector.riotx.features.pin.PinMode import im.vector.riotx.features.themes.ThemeUtils import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import kotlinx.coroutines.launch import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val vectorPreferences: VectorPreferences, + private val pinLocker: PinLocker, private val activeSessionHolder: ActiveSessionHolder, private val pinCodeStore: PinCodeStore, private val navigator: Navigator @@ -298,6 +299,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } } } else if (requestCode == PinActivity.PIN_REQUEST_CODE) { + pinLocker.unlock() refreshPinCodeStatus() } else if (requestCode == REQUEST_E2E_FILE_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) {