diff --git a/CHANGES.md b/CHANGES.md
index 61b5c61caa..f8261576ec 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,7 +2,7 @@ Changes in Element 1.0.4 (2020-XX-XX)
===================================================
Features ✨:
- -
+ - Protect access to the app by a pin code (#1700)
Improvements 🙌:
-
diff --git a/build.gradle b/build.gradle
index 47b3ab240d..12c5181ea4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,6 +41,8 @@ allprojects {
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
// PhotoView
includeGroupByRegex 'com\\.github\\.chrisbanes'
+ // PFLockScreen-Android
+ includeGroupByRegex 'com\\.github\\.ganfra'
}
}
maven {
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index 08142719b9..8d181f1da4 100644
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
-enum class===74
+enum class===76
### Do not import temporary legacy classes
import im.vector.matrix.android.internal.legacy.riot===3
diff --git a/vector/build.gradle b/vector/build.gradle
index dd78e7e87c..db3ec960a6 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -346,6 +346,7 @@ dependencies {
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
+ implementation 'com.github.ganfra:PFLockScreen-Android:1.0.0-beta8'
// Custom Tab
implementation 'androidx.browser:browser:1.2.0'
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 344a10e75b..32ba828d36 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -205,6 +205,7 @@
+
diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
index 241240d7e3..cbed51607c 100644
--- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
@@ -47,6 +47,7 @@ import im.vector.riotx.features.disclaimer.doNotShowDisclaimerDialog
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.PinLocker
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
@@ -82,6 +83,7 @@ class VectorApplication :
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
+ @Inject lateinit var pinLocker: PinLocker
lateinit var vectorComponent: VectorComponent
@@ -153,6 +155,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/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index bc5a985d9f..fbb4cad78b 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -66,6 +66,7 @@ import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment
+import im.vector.riotx.features.pin.PinFragment
import im.vector.riotx.features.qrcode.QrCodeScannerFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
@@ -536,6 +537,11 @@ interface FragmentModule {
@FragmentKey(ContactsBookFragment::class)
fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
+ @Binds
+ @IntoMap
+ @FragmentKey(PinFragment::class)
+ fun bindPinFragment(fragment: PinFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(RoomBannedMemberListFragment::class)
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 36dda5aa39..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
@@ -48,6 +48,8 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
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
@@ -132,12 +134,16 @@ interface VectorComponent {
fun uiStateRepository(): UiStateRepository
+ fun pinCodeStore(): PinCodeStore
+
fun emojiDataSource(): EmojiDataSource
fun alertManager(): PopupAlertManager
fun reAuthHelper(): ReAuthHelper
+ fun pinLocker(): PinLocker
+
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
@Component.Factory
diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
index 50cfeaf415..663e5dbe79 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
@@ -31,6 +31,8 @@ import im.vector.riotx.core.error.DefaultErrorFormatter
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator
+import im.vector.riotx.features.pin.PinCodeStore
+import im.vector.riotx.features.pin.SharedPrefPinCodeStore
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
import im.vector.riotx.features.ui.UiStateRepository
@@ -86,4 +88,7 @@ abstract class VectorModule {
@Binds
abstract fun bindUiStateRepository(repository: SharedPreferencesUiStateRepository): UiStateRepository
+
+ @Binds
+ abstract fun bindPinCodeStore(store: SharedPrefPinCodeStore): PinCodeStore
}
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 59bf7a8aeb..f142b46640 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,21 @@ 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()
+ }
+ else -> {
+ pinLocker.block()
+ moveTaskToBack(true)
+ }
+ }
+ }
+ }
+
override fun onResume() {
super.onResume()
Timber.i("onResume Activity ${this.javaClass.simpleName}")
@@ -294,7 +322,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
rageShake.start()
}
-
DebugReceiver
.getIntentFilter(this)
.takeIf { BuildConfig.DEBUG }
diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
index a5fa8fc4e4..051ae72c94 100644
--- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
@@ -35,6 +35,9 @@ import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.ShortcutsHandler
import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
+import im.vector.riotx.features.pin.PinCodeStore
+import im.vector.riotx.features.pin.PinLocker
+import im.vector.riotx.features.pin.UnlockedActivity
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.signout.hard.SignedOutActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
@@ -61,7 +64,7 @@ data class MainActivityArgs(
* This Activity, when started with argument, is also doing some cleanup when user disconnects,
* clears cache, is logged out, or is soft logged out
*/
-class MainActivity : VectorBaseActivity() {
+class MainActivity : VectorBaseActivity(), UnlockedActivity {
companion object {
private const val EXTRA_ARGS = "EXTRA_ARGS"
@@ -84,6 +87,8 @@ class MainActivity : VectorBaseActivity() {
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var uiStateRepository: UiStateRepository
@Inject lateinit var shortcutsHandler: ShortcutsHandler
+ @Inject lateinit var pinCodeStore: PinCodeStore
+ @Inject lateinit var pinLocker: PinLocker
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@@ -181,6 +186,8 @@ class MainActivity : VectorBaseActivity() {
if (clearPreferences) {
vectorPreferences.clearPreferences()
uiStateRepository.reset()
+ pinLocker.unlock()
+ pinCodeStore.deleteEncodedPin()
}
withContext(Dispatchers.IO) {
// On BG thread
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt
index 7bb7e99c5f..c3f010f6bc 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt
@@ -44,13 +44,14 @@ import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument
import im.vector.riotx.features.login.terms.toLocalizedLoginTerms
+import im.vector.riotx.features.pin.UnlockedActivity
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
/**
* The LoginActivity manages the fragment navigation and also display the loading View
*/
-open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
+open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity {
private val loginViewModel: LoginViewModel by viewModel()
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 8267ba4c99..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
@@ -52,6 +52,9 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.VectorAttachmentViewerActivity
+import im.vector.riotx.features.pin.PinActivity
+import im.vector.riotx.features.pin.PinArgs
+import im.vector.riotx.features.pin.PinMode
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@@ -272,6 +275,16 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
+ override fun openPinCode(fragment: Fragment, pinMode: PinMode, requestCode: Int) {
+ val intent = PinActivity.newIntent(fragment.requireContext(), PinArgs(pinMode))
+ 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 2d817183be..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
@@ -28,6 +28,8 @@ import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.media.AttachmentData
+import im.vector.riotx.features.pin.PinActivity
+import im.vector.riotx.features.pin.PinMode
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@@ -78,6 +80,10 @@ interface Navigator {
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
+ 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/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
index 36874d5782..0889293539 100755
--- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt
@@ -46,6 +46,7 @@ import im.vector.riotx.features.call.service.CallHeadsUpActionReceiver
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
+import im.vector.riotx.features.pin.PinLocker
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
import javax.inject.Inject
@@ -59,6 +60,7 @@ import kotlin.random.Random
@Singleton
class NotificationUtils @Inject constructor(private val context: Context,
private val stringProvider: StringProvider,
+ private val pinLocker: PinLocker,
private val vectorPreferences: VectorPreferences) {
companion object {
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
new file mode 100644
index 0000000000..e9b6191ec4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/pin/PinActivity.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.content.Context
+import android.content.Intent
+import androidx.appcompat.widget.Toolbar
+import com.airbnb.mvrx.MvRx
+import im.vector.riotx.R
+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, UnlockedActivity {
+
+ companion object {
+
+ const val PIN_REQUEST_CODE = 17890
+
+ fun newIntent(context: Context, args: PinArgs): Intent {
+ return Intent(context, PinActivity::class.java).apply {
+ putExtra(MvRx.KEY_ARG, args)
+ }
+ }
+ }
+
+ override fun getLayoutRes() = R.layout.activity_simple
+
+ override fun initUiAndData() {
+ if (isFirstCreation()) {
+ val fragmentArgs: PinArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
+ addFragment(R.id.simpleFragmentContainer, PinFragment::class.java, fragmentArgs)
+ }
+ }
+
+ override fun configure(toolbar: Toolbar) {
+ configureToolbar(toolbar)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/pin/PinCodeStore.kt b/vector/src/main/java/im/vector/riotx/features/pin/PinCodeStore.kt
new file mode 100644
index 0000000000..33e4f22b81
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/pin/PinCodeStore.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.content.SharedPreferences
+import androidx.core.content.edit
+import com.beautycoder.pflockscreen.security.PFResult
+import com.beautycoder.pflockscreen.security.PFSecurityManager
+import com.beautycoder.pflockscreen.security.callbacks.PFPinCodeHelperCallback
+import im.vector.matrix.android.api.extensions.orFalse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+interface PinCodeStore {
+
+ suspend fun storeEncodedPin(encodePin: String)
+
+ suspend fun deleteEncodedPin()
+
+ fun getEncodedPin(): String?
+
+ suspend fun hasEncodedPin(): Boolean
+}
+
+class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore {
+
+ override suspend fun storeEncodedPin(encodePin: String) = withContext(Dispatchers.IO) {
+ sharedPreferences.edit {
+ putString(ENCODED_PIN_CODE_KEY, encodePin)
+ }
+ }
+
+ override suspend fun deleteEncodedPin() = withContext(Dispatchers.IO) {
+ sharedPreferences.edit {
+ remove(ENCODED_PIN_CODE_KEY)
+ }
+ awaitPinCodeCallback {
+ PFSecurityManager.getInstance().pinCodeHelper.delete(it)
+ }
+ return@withContext
+ }
+
+ override fun getEncodedPin(): String? {
+ return sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null)
+ }
+
+ override suspend fun hasEncodedPin(): Boolean = withContext(Dispatchers.IO) {
+ val hasEncodedPin = getEncodedPin()?.isNotBlank().orFalse()
+ if (!hasEncodedPin) {
+ return@withContext false
+ }
+ val result = awaitPinCodeCallback {
+ PFSecurityManager.getInstance().pinCodeHelper.isPinCodeEncryptionKeyExist(it)
+ }
+ result.error == null && result.result
+ }
+
+ private suspend inline fun awaitPinCodeCallback(crossinline callback: (PFPinCodeHelperCallback) -> Unit) = suspendCoroutine> { cont ->
+ callback(PFPinCodeHelperCallback { result -> cont.resume(result) })
+ }
+
+ companion object {
+ const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
+ }
+}
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
new file mode 100644
index 0000000000..58f3dbc2c1
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/pin/PinFragment.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.app.Activity
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import com.airbnb.mvrx.args
+import com.beautycoder.pflockscreen.PFFLockScreenConfiguration
+import com.beautycoder.pflockscreen.fragments.PFLockScreenFragment
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.replaceFragment
+import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.features.MainActivity
+import im.vector.riotx.features.MainActivityArgs
+import kotlinx.android.parcel.Parcelize
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@Parcelize
+data class PinArgs(
+ val pinMode: PinMode
+) : Parcelable
+
+class PinFragment @Inject constructor(
+ private val pinCodeStore: PinCodeStore
+) : VectorBaseFragment() {
+
+ private val fragmentArgs: PinArgs by args()
+
+ override fun getLayoutResId() = R.layout.fragment_pin
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ when (fragmentArgs.pinMode) {
+ PinMode.CREATE -> showCreateFragment()
+ PinMode.DELETE -> showDeleteFragment()
+ PinMode.AUTH -> showAuthFragment()
+ }
+ }
+
+ private fun showDeleteFragment() {
+ val encodedPin = pinCodeStore.getEncodedPin() ?: return
+ val authFragment = PFLockScreenFragment()
+ val builder = PFFLockScreenConfiguration.Builder(requireContext())
+ .setUseFingerprint(true)
+ .setTitle(getString(R.string.auth_pin_confirm_to_disable_title))
+ .setClearCodeOnError(true)
+ .setMode(PFFLockScreenConfiguration.MODE_AUTH)
+ authFragment.setConfiguration(builder.build())
+ authFragment.setEncodedPinCode(encodedPin)
+ authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
+ override fun onPinLoginFailed() {
+ }
+
+ override fun onFingerprintSuccessful() {
+ lifecycleScope.launch {
+ pinCodeStore.deleteEncodedPin()
+ vectorBaseActivity.setResult(Activity.RESULT_OK)
+ vectorBaseActivity.finish()
+ }
+ }
+
+ override fun onFingerprintLoginFailed() {
+ }
+
+ override fun onCodeInputSuccessful() {
+ lifecycleScope.launch {
+ pinCodeStore.deleteEncodedPin()
+ vectorBaseActivity.setResult(Activity.RESULT_OK)
+ vectorBaseActivity.finish()
+ }
+ }
+ })
+ replaceFragment(R.id.pinFragmentContainer, authFragment)
+ }
+
+ private fun showCreateFragment() {
+ val createFragment = PFLockScreenFragment()
+ val builder = PFFLockScreenConfiguration.Builder(requireContext())
+ .setNewCodeValidation(true)
+ .setTitle(getString(R.string.create_pin_title))
+ .setNewCodeValidationTitle(getString(R.string.create_pin_confirm_title))
+ .setMode(PFFLockScreenConfiguration.MODE_CREATE)
+
+ createFragment.setConfiguration(builder.build())
+ createFragment.setCodeCreateListener(object : PFLockScreenFragment.OnPFLockScreenCodeCreateListener {
+ override fun onNewCodeValidationFailed() {
+ Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onCodeCreated(encodedCode: String) {
+ lifecycleScope.launch {
+ pinCodeStore.storeEncodedPin(encodedCode)
+ vectorBaseActivity.setResult(Activity.RESULT_OK)
+ vectorBaseActivity.finish()
+ }
+ }
+ })
+ replaceFragment(R.id.pinFragmentContainer, createFragment)
+ }
+
+ private fun showAuthFragment() {
+ val encodedPin = pinCodeStore.getEncodedPin() ?: return
+ val authFragment = PFLockScreenFragment()
+ val builder = PFFLockScreenConfiguration.Builder(requireContext())
+ .setUseFingerprint(true)
+ .setTitle(getString(R.string.auth_pin_title))
+ .setLeftButton(getString(R.string.auth_pin_forgot))
+ .setClearCodeOnError(true)
+ .setMode(PFFLockScreenConfiguration.MODE_AUTH)
+ authFragment.setConfiguration(builder.build())
+ authFragment.setEncodedPinCode(encodedPin)
+ authFragment.setOnLeftButtonClickListener {
+ displayForgotPinWarningDialog()
+ }
+ authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
+ override fun onPinLoginFailed() {
+ }
+
+ override fun onFingerprintSuccessful() {
+ vectorBaseActivity.setResult(Activity.RESULT_OK)
+ vectorBaseActivity.finish()
+ }
+
+ override fun onFingerprintLoginFailed() {
+ }
+
+ override fun onCodeInputSuccessful() {
+ vectorBaseActivity.setResult(Activity.RESULT_OK)
+ vectorBaseActivity.finish()
+ }
+ })
+ replaceFragment(R.id.pinFragmentContainer, authFragment)
+ }
+
+ private fun displayForgotPinWarningDialog() {
+ AlertDialog.Builder(requireContext())
+ .setTitle(getString(R.string.auth_pin_reset_title))
+ .setMessage(getString(R.string.auth_pin_reset_content))
+ .setPositiveButton(getString(R.string.auth_pin_new_pin_action)) { _, _ ->
+ launchResetPinFlow()
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+
+ private fun launchResetPinFlow() {
+ MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true))
+ }
+}
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..441977138c
--- /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 = 2 * 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/PinMode.kt b/vector/src/main/java/im/vector/riotx/features/pin/PinMode.kt
new file mode 100644
index 0000000000..952a0e3e94
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/pin/PinMode.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+enum class PinMode {
+ CREATE,
+ DELETE,
+ AUTH
+}
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..4177496154
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/pin/UnlockedActivity.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+/**
+ * Tag class for activities that should not be protected by PIN code.
+ */
+interface UnlockedActivity
diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
index e5b2f34f61..25a0e6ad7d 100644
--- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
@@ -28,6 +28,7 @@ import dagger.Lazy
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.pin.PinActivity
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
import java.lang.ref.WeakReference
@@ -84,12 +85,10 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy currentAlerter!!.expirationTimestamp!!) {
// this alert has expired, remove it
@@ -126,7 +125,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy next.expirationTimestamp!!) {
// skip
@@ -250,4 +249,11 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
}
+ private val usePinCodePref by lazy {
+ findPreference(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
+ }
+
override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView {
return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also {
// Insert animation are really annoying the first time the list is shown
@@ -231,6 +244,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
}
+ refreshPinCodeStatus()
+
refreshXSigningStatus()
secureBackupPreference.icon = activity?.let {
@@ -313,10 +328,28 @@ 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) {
+ importKeys(data)
+ }
}
- if (resultCode == Activity.RESULT_OK) {
- when (requestCode) {
- REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
+ }
+
+ private fun refreshPinCodeStatus() {
+ lifecycleScope.launchWhenResumed {
+ val hasPinCode = pinCodeStore.hasEncodedPin()
+ usePinCodePref.isChecked = hasPinCode
+ usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ val pinMode = if (hasPinCode) {
+ PinMode.DELETE
+ } else {
+ PinMode.CREATE
+ }
+ navigator.openPinCode(this@VectorSettingsSecurityPrivacyFragment, pinMode)
+ true
}
}
}
diff --git a/vector/src/main/res/drawable/bg_pin_key.xml b/vector/src/main/res/drawable/bg_pin_key.xml
new file mode 100644
index 0000000000..d4a54577be
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_pin_key.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/pin_code_dot_empty.xml b/vector/src/main/res/drawable/pin_code_dot_empty.xml
new file mode 100644
index 0000000000..6ee800a07d
--- /dev/null
+++ b/vector/src/main/res/drawable/pin_code_dot_empty.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/pin_code_dot_fill.xml b/vector/src/main/res/drawable/pin_code_dot_fill.xml
new file mode 100644
index 0000000000..799ea30174
--- /dev/null
+++ b/vector/src/main/res/drawable/pin_code_dot_fill.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/pin_code_dots.xml b/vector/src/main/res/drawable/pin_code_dots.xml
new file mode 100644
index 0000000000..29e445e511
--- /dev/null
+++ b/vector/src/main/res/drawable/pin_code_dots.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_pin.xml b/vector/src/main/res/layout/fragment_pin.xml
new file mode 100644
index 0000000000..0672ce4c7e
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_pin.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 6dc4faab27..bf2ce1f7a9 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2572,4 +2572,15 @@ Not all features in Riot are implemented in Element yet. Main missing (and comin
Push notifications are disabled
Review your settings to enable push notifications
+ Choose a PIN for security
+ Confirm PIN
+ Failed to validate pin, please tap a new one.
+ Enter your PIN
+ Forgot PIN?
+ Reset pin
+ New pin
+ To reset your PIN, you\'ll need to re-login and create a new one.
+ Enable PIN
+ If you want to reset your PIN, tap Forgot PIN to logout and reset.
+ Confirm PIN to disable PIN
diff --git a/vector/src/main/res/values/styles_pin_code.xml b/vector/src/main/res/values/styles_pin_code.xml
new file mode 100644
index 0000000000..ecf217bf03
--- /dev/null
+++ b/vector/src/main/res/values/styles_pin_code.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml
index fae33cf6bf..a391e48cfb 100644
--- a/vector/src/main/res/values/theme_dark.xml
+++ b/vector/src/main/res/values/theme_dark.xml
@@ -222,6 +222,16 @@
- @style/VectorSnackBarButton
- @style/VectorSnackBarText
+
+ - @style/PinCodeScreenStyle
+ - @style/PinCodeKeyButtonStyle
+ - @style/PinCodeTitleStyle
+ - @style/PinCodeHintStyle
+ - @style/PinCodeDotsViewStyle
+ - @style/PinCodeDeleteButtonStyle
+ - @style/PinCodeFingerprintButtonStyle
+ - @style/PinCodeNextButtonStyle
+
diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml
index 28b29094e5..cfd7b7cc81 100644
--- a/vector/src/main/res/values/theme_light.xml
+++ b/vector/src/main/res/values/theme_light.xml
@@ -222,6 +222,16 @@
- @style/VectorSnackBarButton
- @style/VectorSnackBarText
+
+ - @style/PinCodeScreenStyle
+ - @style/PinCodeKeyButtonStyle
+ - @style/PinCodeTitleStyle
+ - @style/PinCodeHintStyle
+ - @style/PinCodeDotsViewStyle
+ - @style/PinCodeDeleteButtonStyle
+ - @style/PinCodeFingerprintButtonStyle
+ - @style/PinCodeNextButtonStyle
+
diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml
index 5f249fb358..1c021966c4 100644
--- a/vector/src/main/res/xml/vector_settings_security_privacy.xml
+++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml
@@ -117,6 +117,12 @@
android:summary="@string/settings_security_prevent_screenshots_summary"
android:title="@string/settings_security_prevent_screenshots_title" />
+
+
\ No newline at end of file