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 +