Merge pull request #1844 from vector-im/feature/pin_code

Feature/pin code
This commit is contained in:
ganfra 2020-07-31 11:51:39 +02:00 committed by GitHub
commit 7f326abcd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 742 additions and 15 deletions

View file

@ -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 🙌:
-

View file

@ -41,6 +41,8 @@ allprojects {
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
// PhotoView
includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.ganfra'
}
}
maven {

View file

@ -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

View file

@ -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'

View file

@ -205,6 +205,7 @@
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" />
<activity android:name=".features.pin.PinActivity" />
<!-- Services -->

View file

@ -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)
}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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 }

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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<Boolean> {
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<Boolean> {
PFSecurityManager.getInstance().pinCodeHelper.isPinCodeEncryptionKeyExist(it)
}
result.error == null && result.result
}
private suspend inline fun <T> awaitPinCodeCallback(crossinline callback: (PFPinCodeHelperCallback<T>) -> Unit) = suspendCoroutine<PFResult<T>> { cont ->
callback(PFPinCodeHelperCallback<T> { result -> cont.resume(result) })
}
companion object {
const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
}
}

View file

@ -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))
}
}

View file

@ -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<State>()
private var isBlocked = false
private var shouldBeLocked = true
private var entersBackgroundTs = 0L
fun getLiveState(): LiveData<State> {
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()
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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<Ava
setLightStatusBar()
}
}
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false || activity !is VectorBaseActivity) {
weakCurrentActivity = WeakReference(activity)
if (!shouldBeDisplayedIn(currentAlerter, activity)) {
return
}
weakCurrentActivity = WeakReference(activity)
if (currentAlerter != null) {
if (currentAlerter!!.expirationTimestamp != null && System.currentTimeMillis() > currentAlerter!!.expirationTimestamp!!) {
// this alert has expired, remove it
@ -126,7 +125,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
}
currentAlerter = next
next?.let {
if (next.shouldBeDisplayedIn?.invoke(currentActivity) == false) return
if (!shouldBeDisplayedIn(next, currentActivity)) return
val currentTime = System.currentTimeMillis()
if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) {
// skip
@ -250,4 +249,11 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
displayNextIfPossible()
}, 500)
}
private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean {
return alert != null
&& activity !is PinActivity
&& activity is VectorBaseActivity
&& alert.shouldBeDisplayedIn?.invoke(activity) == true
}
}

View file

@ -160,6 +160,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
// Security
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
// other
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@ -812,4 +813,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
fun useFlagSecure(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_FLAG_SECURE, false)
}
/**
* The user enable protecting app access with pin code
*/
fun useFlagPinCode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false)
}
}

View file

@ -28,6 +28,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference
@ -56,6 +57,11 @@ import im.vector.riotx.features.crypto.keys.KeysExporter
import im.vector.riotx.features.crypto.keys.KeysImporter
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
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
@ -64,7 +70,10 @@ import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
private val pinLocker: PinLocker,
private val activeSessionHolder: ActiveSessionHolder,
private val pinCodeStore: PinCodeStore,
private val navigator: Navigator
) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_and_privacy
@ -101,6 +110,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
}
private val usePinCodePref by lazy {
findPreference<SwitchPreference>(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
}
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item
android:gravity="center">
<shape android:shape="oval">
<solid android:color="#44FFFFFF" />
<size
android:width="70dp"
android:height="70dp" />
</shape>
</item>
</ripple>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:color="?riotx_text_secondary"
android:width="1px"/>
<size
android:width="24dp"
android:height="24dp"/>
</shape>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="?colorPrimary"/>
<size
android:width="24dp"
android:height="24dp"/>
</shape>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- NOTE: order is important (the first matching state(s) is what is rendered) -->
<item
android:state_checked="true"
android:drawable="@drawable/pin_code_dot_fill"/>
<item
android:drawable="@drawable/pin_code_dot_empty"/>
</selector>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/pinFragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2572,4 +2572,15 @@ Not all features in Riot are implemented in Element yet. Main missing (and comin
<string name="alert_push_are_disabled_title">Push notifications are disabled</string>
<string name="alert_push_are_disabled_description">Review your settings to enable push notifications</string>
<string name="create_pin_title">Choose a PIN for security</string>
<string name="create_pin_confirm_title">Confirm PIN</string>
<string name="create_pin_confirm_failure">Failed to validate pin, please tap a new one.</string>
<string name="auth_pin_title">Enter your PIN</string>
<string name="auth_pin_forgot">Forgot PIN?</string>
<string name="auth_pin_reset_title">Reset pin</string>
<string name="auth_pin_new_pin_action">New pin</string>
<string name="auth_pin_reset_content">To reset your PIN, you\'ll need to re-login and create a new one.</string>
<string name="settings_security_pin_code_title">Enable PIN</string>
<string name="settings_security_pin_code_summary">If you want to reset your PIN, tap Forgot PIN to logout and reset.</string>
<string name="auth_pin_confirm_to_disable_title">Confirm PIN to disable PIN</string>
</resources>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="PinCodeScreenStyle" >
<item name="android:background">?riotx_background</item>
</style>
<style name="PinCodeKeyButtonStyle">
<item name="android:textColor">?riotx_text_primary</item>
<item name="android:textSize">18sp</item>
<item name="android:background">@drawable/bg_pin_key</item>
</style>
<style name="PinCodeDotsViewStyle">
<item name="android:button">@drawable/pin_code_dots</item>
</style>
<style name="PinCodeNextButtonStyle" parent="VectorButtonStylePositive">
<item name="android:textSize">18sp</item>
<item name="android:backgroundTint">@android:color/transparent</item>
<item name="android:layout_marginBottom">24dp</item>
</style>
<style name="PinCodeDeleteButtonStyle">
<item name="android:src">@drawable/delete_lockscreen_pf</item>
<item name="android:tint">?riotx_text_primary</item>
<item name="background">@drawable/bg_pin_key</item>
</style>
<style name="PinCodeFingerprintButtonStyle">
<item name="android:src">@drawable/fingerprint_lockscreen_pf</item>
<item name="android:tint">?riotx_text_primary</item>
<item name="background">@drawable/bg_pin_key</item>
</style>
<style name="PinCodeTitleStyle">
<item name="android:textColor">?riotx_text_primary</item>
</style>
<style name="PinCodeHintStyle">
<item name="android:textColor">?riotx_text_primary</item>
</style>
</resources>

View file

@ -222,6 +222,16 @@
<item name="snackbarButtonStyle">@style/VectorSnackBarButton</item>
<!-- Style to use for message text within a SnackBar in this theme. -->
<item name="snackbarTextViewStyle">@style/VectorSnackBarText</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
<item name="pf_title">@style/PinCodeTitleStyle</item>
<item name="pf_hint">@style/PinCodeHintStyle</item>
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
</style>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View file

@ -222,6 +222,16 @@
<item name="snackbarButtonStyle">@style/VectorSnackBarButton</item>
<!-- Style to use for message text within a SnackBar in this theme. -->
<item name="snackbarTextViewStyle">@style/VectorSnackBarText</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
<item name="pf_title">@style/PinCodeTitleStyle</item>
<item name="pf_hint">@style/PinCodeHintStyle</item>
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
</style>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" />

View file

@ -117,6 +117,12 @@
android:summary="@string/settings_security_prevent_screenshots_summary"
android:title="@string/settings_security_prevent_screenshots_title" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
android:summary="@string/settings_security_pin_code_summary"
android:title="@string/settings_security_pin_code_title" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen>