Improve the parent key setup flow

This commit is contained in:
Jonas Lochmann 2022-05-09 02:00:00 +02:00
parent 9c887372f7
commit f7991e6dbf
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
9 changed files with 204 additions and 209 deletions

View file

@ -0,0 +1,35 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.extension
import android.widget.ViewFlipper
import io.timelimit.android.R
fun ViewFlipper.openNextWizardScreen(index: Int) {
if (displayedChild != index) {
setInAnimation(context, R.anim.wizard_open_step_in)
setOutAnimation(context, R.anim.wizard_open_step_out)
displayedChild = index
}
}
fun ViewFlipper.openPreviousWizardScreen(index: Int) {
if (displayedChild != index) {
setInAnimation(context, R.anim.wizard_close_step_in)
setOutAnimation(context, R.anim.wizard_close_step_out)
displayedChild = index
}
}

View file

@ -1,39 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.login
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.ui.manage.parent.key.ScanKeyDialogFragment
import io.timelimit.android.ui.manage.parent.key.ScannedKey
class CodeLoginDialogFragment: ScanKeyDialogFragment() {
companion object {
private const val DIALOG_TAG = "CodeLoginDialogFragment"
}
override fun handleResult(key: ScannedKey?) {
if (key == null) {
Toast.makeText(context!!, R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
} else {
(targetFragment as NewLoginFragment).tryCodeLogin(key)
}
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -15,6 +15,7 @@
*/
package io.timelimit.android.ui.login
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Build
@ -26,8 +27,8 @@ import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialog
import io.timelimit.android.R
@ -36,8 +37,12 @@ import io.timelimit.android.data.model.User
import io.timelimit.android.databinding.NewLoginFragmentBinding
import io.timelimit.android.extensions.setOnEnterListenr
import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.extension.openNextWizardScreen
import io.timelimit.android.ui.extension.openPreviousWizardScreen
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.parent.key.MissingBarcodeScannerDialogFragment
import io.timelimit.android.ui.manage.parent.key.ScanBarcode
import io.timelimit.android.ui.manage.parent.key.ScannedKey
import io.timelimit.android.ui.view.KeyboardViewListener
@ -63,15 +68,23 @@ class NewLoginFragment: DialogFragment() {
private const val WAITING_FOR_SYNC = 8
}
private val model: LoginDialogFragmentModel by lazy {
ViewModelProviders.of(this).get(LoginDialogFragmentModel::class.java)
}
private val model: LoginDialogFragmentModel by viewModels()
private val inputMethodManager: InputMethodManager by lazy {
context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
private val activityModelHolder get() = requireActivity() as ActivityViewModelHolder
private val scanLoginCode = registerForActivityResult(ScanBarcode()) { barcode ->
barcode ?: return@registerForActivityResult
ScannedKey.tryDecode(barcode).let { key ->
if (key == null) Toast.makeText(requireContext(), R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
else tryCodeLogin(key)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -80,11 +93,11 @@ class NewLoginFragment: DialogFragment() {
}
if (savedInstanceState == null) {
model.tryDefaultLogin(getActivityViewModel(activity!!))
model.tryDefaultLogin(getActivityViewModel(requireActivity()))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?) = object: BottomSheetDialog(context!!, theme) {
override fun onCreateDialog(savedInstanceState: Bundle?) = object: BottomSheetDialog(requireContext(), theme) {
override fun onBackPressed() {
if (!model.goBack()) {
super.onBackPressed()
@ -130,9 +143,11 @@ class NewLoginFragment: DialogFragment() {
}
override fun onScanCodeRequested() {
CodeLoginDialogFragment().apply {
setTargetFragment(this@NewLoginFragment, 0)
}.show(parentFragmentManager)
try {
scanLoginCode.launch(null)
} catch (ex: ActivityNotFoundException) {
MissingBarcodeScannerDialogFragment.newInstance().show(parentFragmentManager)
}
}
}
@ -158,7 +173,7 @@ class NewLoginFragment: DialogFragment() {
model.tryParentLogin(
password = password.text.toString(),
keepSignedIn = checkDontAskAgain.isChecked,
model = getActivityViewModel(activity!!),
model = getActivityViewModel(requireActivity()),
setAsDeviceUser = checkAssignMyself.isChecked
)
}
@ -203,11 +218,7 @@ class NewLoginFragment: DialogFragment() {
dismissAllowingStateLoss()
}
is UserListLoginDialogStatus -> {
if (binding.switcher.displayedChild != USER_LIST) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_close_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_close_step_out)
binding.switcher.displayedChild = USER_LIST
}
binding.switcher.openPreviousWizardScreen(USER_LIST)
val users = status.usersToShow.map { LoginUserAdapterUser(it) }
@ -221,11 +232,7 @@ class NewLoginFragment: DialogFragment() {
null
}
is ParentUserLogin -> {
if (binding.switcher.displayedChild != PARENT_AUTH) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = PARENT_AUTH
}
binding.switcher.openNextWizardScreen(PARENT_AUTH)
binding.enterPassword.password.isEnabled = !status.isCheckingPassword
@ -255,7 +262,7 @@ class NewLoginFragment: DialogFragment() {
}
if (status.wasPasswordWrong) {
Toast.makeText(context!!, R.string.login_snackbar_wrong, Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), R.string.login_snackbar_wrong, Toast.LENGTH_SHORT).show()
binding.enterPassword.password.setText("")
model.resetPasswordWrong()
@ -264,40 +271,24 @@ class NewLoginFragment: DialogFragment() {
null
}
ParentUserLoginMissingTrustedTime -> {
if (binding.switcher.displayedChild != UNVERIFIED_TIME) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = UNVERIFIED_TIME
}
binding.switcher.openNextWizardScreen(UNVERIFIED_TIME)
null
}
is CanNotSignInChildHasNoPassword -> {
if (binding.switcher.displayedChild != CHILD_MISSING_PASSWORD) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = CHILD_MISSING_PASSWORD
}
binding.switcher.openNextWizardScreen(CHILD_MISSING_PASSWORD)
binding.childWithoutPassword.childName = status.childName
null
}
is ChildAlreadyDeviceUser -> {
if (binding.switcher.displayedChild != CHILD_ALREADY_CURRENT_USER) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = CHILD_ALREADY_CURRENT_USER
}
binding.switcher.openNextWizardScreen(CHILD_ALREADY_CURRENT_USER)
null
}
is ChildUserLogin -> {
if (binding.switcher.displayedChild != CHILD_AUTH) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = CHILD_AUTH
}
binding.switcher.openNextWizardScreen(CHILD_AUTH)
binding.childPassword.password.requestFocus()
inputMethodManager.showSoftInput(binding.childPassword.password, 0)
@ -305,7 +296,7 @@ class NewLoginFragment: DialogFragment() {
binding.childPassword.password.isEnabled = !status.isCheckingPassword
if (status.wasPasswordWrong) {
Toast.makeText(context!!, R.string.login_snackbar_wrong, Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), R.string.login_snackbar_wrong, Toast.LENGTH_SHORT).show()
binding.childPassword.password.setText("")
model.resetPasswordWrong()
@ -314,32 +305,20 @@ class NewLoginFragment: DialogFragment() {
null
}
ChildLoginRequiresPremiumStatus -> {
if (binding.switcher.displayedChild != CHILD_LOGIN_REQUIRES_PREMIUM) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = CHILD_LOGIN_REQUIRES_PREMIUM
}
binding.switcher.openNextWizardScreen(CHILD_LOGIN_REQUIRES_PREMIUM)
null
}
is ParentUserLoginBlockedByCategory -> {
if (binding.switcher.displayedChild != PARENT_LOGIN_BLOCKED) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = PARENT_LOGIN_BLOCKED
}
binding.switcher.openNextWizardScreen(PARENT_LOGIN_BLOCKED)
binding.parentLoginBlocked.categoryTitle = status.categoryTitle
binding.parentLoginBlocked.reason = LoginDialogFragmentModel.formatBlockingReasonForLimitLoginCategory(status.reason, context!!)
binding.parentLoginBlocked.reason = LoginDialogFragmentModel.formatBlockingReasonForLimitLoginCategory(status.reason, requireContext())
null
}
ParentUserLoginWaitingForSync -> {
if (binding.switcher.displayedChild != WAITING_FOR_SYNC) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = WAITING_FOR_SYNC
}
binding.switcher.openNextWizardScreen(WAITING_FOR_SYNC)
null
}
@ -350,6 +329,6 @@ class NewLoginFragment: DialogFragment() {
}
fun tryCodeLogin(code: ScannedKey) {
model.tryCodeLogin(code, getActivityViewModel(activity!!))
model.tryCodeLogin(code, getActivityViewModel(requireActivity()))
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -15,8 +15,12 @@
*/
package io.timelimit.android.ui.manage.parent.key
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.timelimit.android.R
import io.timelimit.android.async.Threads
@ -24,7 +28,7 @@ import io.timelimit.android.data.model.UserKey
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.DefaultAppLogic
class AddUserKeyDialogFragment: ScanKeyDialogFragment() {
class AddUserKeyDialogFragment: DialogFragment() {
companion object {
private const val DIALOG_TAG = "AddUserKeyDialogFragment"
private const val USER_ID = "userId"
@ -36,13 +40,39 @@ class AddUserKeyDialogFragment: ScanKeyDialogFragment() {
}
}
override fun handleResult(key: ScannedKey?) {
private val scanBarcode = registerForActivityResult(ScanBarcode()) { result ->
if (result != null) handleResult(ScannedKey.tryDecode(result))
dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
.setTitle(R.string.manage_user_key_add)
.setMessage(R.string.manage_user_key_info)
.setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_go, null)
.create()
.also { dialog ->
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
try {
scanBarcode.launch(null)
} catch (ex: ActivityNotFoundException) {
MissingBarcodeScannerDialogFragment.newInstance().show(parentFragmentManager)
dismiss()
}
}
}
}
private fun handleResult(key: ScannedKey?) {
if (key == null) {
Toast.makeText(context!!, R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
} else {
val context = context!!.applicationContext
val context = requireContext().applicationContext
val database = DefaultAppLogic.with(context!!).database
val userId = arguments!!.getString(USER_ID)!!
val userId = requireArguments().getString(USER_ID)!!
Threads.database.execute {
database.runInTransaction {

View file

@ -0,0 +1,57 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.key
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe
class MissingBarcodeScannerDialogFragment: DialogFragment() {
companion object {
private const val DIALOG_TAG = "MissingBarcodeScannerDialogFragment"
fun newInstance() = MissingBarcodeScannerDialogFragment()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
.setTitle(R.string.scan_key_missing_title)
.setMessage(R.string.scan_key_missing_text)
.setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.scan_key_missing_install) { _, _ ->
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=de.markusfisch.android.binaryeye")
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} catch (ex: ActivityNotFoundException) {
Toast.makeText(requireContext(), R.string.error_general, Toast.LENGTH_SHORT).show()
}
}
.create()
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -0,0 +1,33 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.key
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class ScanBarcode: ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent = Intent()
.setPackage("de.markusfisch.android.binaryeye")
.setAction("com.google.zxing.client.android.SCAN")
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("SCAN_RESULT")
} else null
}
}

View file

@ -1,102 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.key
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import io.timelimit.android.R
abstract class ScanKeyDialogFragment: DialogFragment() {
companion object {
private const val CAN_NOT_SCAN = "canNotScan"
private const val REQ_SCAN = 1
}
private var canNotScan = false
final override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
try {
startActivityForResult(
Intent()
.setPackage("de.markusfisch.android.binaryeye")
.setAction("com.google.zxing.client.android.SCAN"),
REQ_SCAN
)
} catch (ex: ActivityNotFoundException) {
canNotScan = true
}
} else {
canNotScan = savedInstanceState.getBoolean(CAN_NOT_SCAN)
}
}
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
if (canNotScan) {
return AlertDialog.Builder(context!!, theme)
.setTitle(R.string.scan_key_missing_title)
.setMessage(R.string.scan_key_missing_text)
.setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.scan_key_missing_install) { _, _ ->
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=de.markusfisch.android.binaryeye")
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} catch (ex: ActivityNotFoundException) {
Toast.makeText(context!!, R.string.error_general, Toast.LENGTH_SHORT).show()
}
}
.create()
} else {
return super.onCreateDialog(savedInstanceState)
}
}
final override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(CAN_NOT_SCAN, canNotScan)
}
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_SCAN && (!canNotScan)) {
if (resultCode == Activity.RESULT_OK) {
val key = ScannedKey.tryDecode(data?.getStringExtra("SCAN_RESULT") ?: "")
handleResult(key)
}
dismiss()
}
}
abstract fun handleResult(key: ScannedKey?)
}

View file

@ -1064,7 +1064,8 @@
<string name="manage_user_key_title">Benutzerschlüssel</string>
<string name="manage_user_key_info">Hiermit kann sich ein Benutzer durch
das Scannen eines Barcodes anmelden. Der gescannte Code muss von TimeLimit
generiert worden sein.
generiert worden sein. Dafür muss TimeLimit auf einem anderen Gerät als
Schlüsselgenerator eingerichtet sein.
</string>
<string name="manage_user_key_not_enrolled">Es gibt keinen Schlüssel für diesen Benutzer</string>
<string name="manage_user_key_is_enrolled">Dieser Benutzer hat den Schlüssel %s</string>

View file

@ -1109,10 +1109,11 @@
<string name="manage_user_key_title">User key</string>
<string name="manage_user_key_info">A key is a barcode which allows the user
to sign in by scanning it. This code must be generated by TimeLimit.
This requires TimeLimit at another device configured as key generator.
</string>
<string name="manage_user_key_not_enrolled">There is no key for this user</string>
<string name="manage_user_key_is_enrolled">This user has got the key %s</string>
<string name="manage_user_key_add">add key</string>
<string name="manage_user_key_add">Add Key</string>
<string name="manage_user_key_remove">remove key</string>
<string name="manage_user_key_added">The key was added</string>
<string name="manage_user_key_invalid">This was no valid TimeLimit key</string>