Skip to content

Commit

Permalink
Small layout changes, code deduplication
Browse files Browse the repository at this point in the history
- Add extensions for duplicated code in dialogs with a text input.
- Remove "Show password" checkbox, instead use the builtin password toggle icon.
- Change IME action in import password dialog.
- Remove deprecated LayoutInflater.fromContext.
- Shorten encrypted export summary (the encryption algorithm doesn't matter much to the average user).
  • Loading branch information
maltaisn committed Jan 6, 2023
1 parent f87ae1e commit 1e46172
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 148 deletions.
75 changes: 75 additions & 0 deletions app/src/main/kotlin/com/maltaisn/notes/DialogExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2023 Nicolas Maltais
*
* 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 com.maltaisn.notes

import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/**
* Set [this] dialog maximum width to [maxWidth].
* @param view The dialog's content view.
*/
fun Dialog.setMaxWidth(maxWidth: Int, view: View) {
// Get current dialog's width and padding
val fgPadding = Rect()
val window = this.window!!
window.decorView.background.getPadding(fgPadding)
val padding = fgPadding.left + fgPadding.right
var width = this.context.resources.displayMetrics.widthPixels - padding

// Set dialog's dimensions, with maximum width.
if (width > maxWidth) {
width = maxWidth
}
window.setLayout(width + padding, FrameLayout.LayoutParams.WRAP_CONTENT)
view.layoutParams = FrameLayout.LayoutParams(width, FrameLayout.LayoutParams.WRAP_CONTENT)
}

/**
* Set title on dialog, but only if the device vertical size is large enough.
* Otherwise, it becomes much harder or even impossible to type text (see #53).
*/
fun MaterialAlertDialogBuilder.setTitleIfEnoughSpace(@StringRes title: Int): MaterialAlertDialogBuilder {
val dimen = context.resources.getDimension(R.dimen.input_dialog_title_min_height) /
context.resources.displayMetrics.density
if (context.resources.configuration.screenHeightDp >= dimen) {
setTitle(title)
}
return this
}

/**
* In dialogs with an EditText, the cursor must be hidden when dialog is dismissed to prevent memory leak.
* See [https://stackoverflow.com/questions/36842805/dialogfragment-leaking-memory].
* This should be called in [DialogFragment.onDismiss].
*/
@SuppressLint("WrongConstant")
fun DialogFragment.hideCursorInAllViews() {
val view = dialog?.window?.decorView ?: return
for (focusableView in view.getFocusables(View.FOCUSABLES_ALL)) {
if (focusableView is TextView) {
focusableView.isCursorVisible = false
}
}
}
26 changes: 1 addition & 25 deletions app/src/main/kotlin/com/maltaisn/notes/FragmentExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Nicolas Maltais
* Copyright 2023 Nicolas Maltais
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,33 +16,9 @@

package com.maltaisn.notes

import android.app.Dialog
import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.FragmentManager

/**
* Returns whether this fragment manager contains a fragment with a [tag].
*/
operator fun FragmentManager.contains(tag: String) = this.findFragmentByTag(tag) != null

/**
* Set [this] dialog maximum width to [maxWidth].
* @param view The dialog's content view.
*/
fun Dialog.setMaxWidth(maxWidth: Int, view: View) {
// Get current dialog's width and padding
val fgPadding = Rect()
val window = this.window!!
window.decorView.background.getPadding(fgPadding)
val padding = fgPadding.left + fgPadding.right
var width = this.context.resources.displayMetrics.widthPixels - padding

// Set dialog's dimensions, with maximum width.
if (width > maxWidth) {
width = maxWidth
}
window.setLayout(width + padding, FrameLayout.LayoutParams.WRAP_CONTENT)
view.layoutParams = FrameLayout.LayoutParams(width, FrameLayout.LayoutParams.WRAP_CONTENT)
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ class DefaultJsonManager @Inject constructor(
// Encrypt notesData
val ciphertext = cipher.doFinal(json.encodeToString(notesData).toByteArray(Charsets.UTF_8))

// Generate BackupData object
return EncryptedNotesData(
salt = prefs.encryptedExportKeyDerivationSalt,
nonce = Base64.encodeToString(cipher.iv, BASE64_FLAGS),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.maltaisn.notes.ui.labels
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
Expand All @@ -28,7 +27,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.maltaisn.notes.App
import com.maltaisn.notes.R
import com.maltaisn.notes.databinding.DialogLabelEditBinding
import com.maltaisn.notes.hideCursorInAllViews
import com.maltaisn.notes.model.entity.Label
import com.maltaisn.notes.setTitleIfEnoughSpace
import com.maltaisn.notes.ui.SharedViewModel
import com.maltaisn.notes.ui.navGraphViewModel
import com.maltaisn.notes.ui.observeEvent
Expand Down Expand Up @@ -56,7 +57,7 @@ class LabelEditDialog : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
val binding = DialogLabelEditBinding.inflate(LayoutInflater.from(context), null, false)
val binding = DialogLabelEditBinding.inflate(layoutInflater, null, false)

// Using `this` as lifecycle owner, cannot show dialog twice with same instance to avoid double observation.
debugCheck(!viewModel.setLabelEvent.hasObservers()) { "Dialog was shown twice with same instance." }
Expand All @@ -71,18 +72,11 @@ class LabelEditDialog : DialogFragment() {
viewModel.addLabel()
}
.setNegativeButton(R.string.action_cancel, null)

// Only show dialog title if screen size is under a certain dimension.
// Otherwise it becomes much harder to type text, see #53.
val dimen = context.resources.getDimension(R.dimen.label_edit_dialog_title_min_height) /
context.resources.displayMetrics.density
if (context.resources.configuration.screenHeightDp >= dimen) {
builder.setTitle(if (args.labelId == Label.NO_ID) {
.setTitleIfEnoughSpace(if (args.labelId == Label.NO_ID) {
R.string.label_create
} else {
R.string.label_edit
})
}

val dialog = builder.create()

Expand All @@ -103,13 +97,7 @@ class LabelEditDialog : DialogFragment() {
viewModel.onHiddenChanged(isChecked)
}

// Cursor must be hidden when dialog is dimissed to prevent memory leak
// See [https://stackoverflow.com/questions/36842805/dialogfragment-leaking-memory]
nameInput.isCursorVisible = true
dialog.setOnDismissListener {
nameInput.isCursorVisible = false
}

nameInput.doAfterTextChanged {
viewModel.onNameChanged(it?.toString() ?: "")
}
Expand All @@ -126,4 +114,9 @@ class LabelEditDialog : DialogFragment() {

return dialog
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
hideCursorInAllViews()
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
/*
* Copyright 2023 Nicolas Maltais
*
* 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 com.maltaisn.notes.ui.settings

import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.maltaisn.notes.App
import com.maltaisn.notes.R
import com.maltaisn.notes.databinding.DialogExportPasswordBinding
import com.maltaisn.notes.hideCursorInAllViews
import com.maltaisn.notes.setTitleIfEnoughSpace
import com.maltaisn.notes.ui.observeEvent
import com.maltaisn.notes.ui.viewModel
import javax.inject.Inject
Expand All @@ -29,12 +45,11 @@ class ExportPasswordDialog : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
val binding = DialogExportPasswordBinding.inflate(LayoutInflater.from(context), null, false)
val binding = DialogExportPasswordBinding.inflate(layoutInflater, null, false)

val passwordInput = binding.passwordInput
val passwordRepeatInput = binding.passwordRepeat
val passwordRepeatLayout = binding.passwordRepeatLayout
val showPasswordCheckbox = binding.showPasswordChk

val builder = MaterialAlertDialogBuilder(context)
.setView(binding.root)
Expand All @@ -45,14 +60,7 @@ class ExportPasswordDialog : DialogFragment() {
.setNegativeButton(R.string.action_cancel) { _, _ ->
callback.onExportPasswordDialogNegativeButtonClicked()
}

// Only show dialog title if screen size is under a certain dimension.
// Otherwise it becomes much harder to type text, see #53.
val dimen = context.resources.getDimension(R.dimen.label_edit_dialog_title_min_height) /
context.resources.displayMetrics.density
if (context.resources.configuration.screenHeightDp >= dimen) {
builder.setTitle(R.string.encrypted_export_dialog_title)
}
.setTitleIfEnoughSpace(R.string.encrypted_export_dialog_title)

val dialog = builder.create()
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
Expand All @@ -72,19 +80,6 @@ class ExportPasswordDialog : DialogFragment() {
}
}

showPasswordCheckbox.setOnCheckedChangeListener { _, isChecked ->
val transformationMethod = if (isChecked) null else PasswordTransformationMethod()
passwordInput.transformationMethod = transformationMethod
passwordRepeatInput.transformationMethod = transformationMethod
}

// Cursor must be hidden when dialog is dismissed to prevent memory leak
// See [https://stackoverflow.com/questions/36842805/dialogfragment-leaking-memory]
dialog.setOnDismissListener {
passwordInput.isCursorVisible = false
passwordRepeatInput.isCursorVisible = false
}

passwordInput.doAfterTextChanged {
viewModel.onPasswordChanged(it?.toString() ?: "", passwordRepeatInput.text.toString())
}
Expand All @@ -102,6 +97,11 @@ class ExportPasswordDialog : DialogFragment() {
return dialog
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
hideCursorInAllViews()
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
callback.onExportPasswordDialogCancelled()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
/*
* Copyright 2023 Nicolas Maltais
*
* 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 com.maltaisn.notes.ui.settings

import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.maltaisn.notes.App
import com.maltaisn.notes.R
import com.maltaisn.notes.databinding.DialogImportPasswordBinding
import com.maltaisn.notes.hideCursorInAllViews
import com.maltaisn.notes.setTitleIfEnoughSpace
import com.maltaisn.notes.ui.observeEvent
import com.maltaisn.notes.ui.viewModel
import javax.inject.Inject
Expand All @@ -29,10 +45,9 @@ class ImportPasswordDialog : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
val binding = DialogImportPasswordBinding.inflate(LayoutInflater.from(context), null, false)
val binding = DialogImportPasswordBinding.inflate(layoutInflater, null, false)

val passwordInput = binding.passwordInput
val showPasswordCheckbox = binding.showPasswordChk

val builder = MaterialAlertDialogBuilder(context)
.setView(binding.root)
Expand All @@ -43,30 +58,12 @@ class ImportPasswordDialog : DialogFragment() {
.setNegativeButton(R.string.action_cancel) { _, _ ->
callback.onImportPasswordDialogNegativeButtonClicked()
}

// Only show dialog title if screen size is under a certain dimension.
// Otherwise it becomes much harder to type text, see #53.
val dimen = context.resources.getDimension(R.dimen.label_edit_dialog_title_min_height) /
context.resources.displayMetrics.density
if (context.resources.configuration.screenHeightDp >= dimen) {
builder.setTitle(R.string.encrypted_import_dialog_title)
}
.setTitleIfEnoughSpace(R.string.encrypted_import_dialog_title)

val dialog = builder.create()
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
dialog.setCanceledOnTouchOutside(true)

showPasswordCheckbox.setOnCheckedChangeListener { _, isChecked ->
val transformationMethod = if (isChecked) null else PasswordTransformationMethod()
passwordInput.transformationMethod = transformationMethod
}

// Cursor must be hidden when dialog is dismissed to prevent memory leak
// See [https://stackoverflow.com/questions/36842805/dialogfragment-leaking-memory]
dialog.setOnDismissListener {
passwordInput.isCursorVisible = false
}

passwordInput.doAfterTextChanged {
val okBtn = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
okBtn.isEnabled = it?.isNotEmpty() ?: false
Expand All @@ -83,6 +80,11 @@ class ImportPasswordDialog : DialogFragment() {
return dialog
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
hideCursorInAllViews()
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
callback.onImportPasswordDialogCancelled()
Expand Down
Loading

0 comments on commit 1e46172

Please sign in to comment.