Fix code quality issues and refactor password generation

This commit is contained in:
Pavel Baksy 2026-03-02 23:25:45 +01:00
parent 898f4aa262
commit dff3db885d
7 changed files with 180 additions and 80 deletions

View File

@ -7,7 +7,8 @@ import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlin.random.Random
private const val KEY_PASSWORD = "password"
class MainActivity : AppCompatActivity() {
@ -37,8 +38,12 @@ class MainActivity : AppCompatActivity() {
generatePassword()
}
// Initially generate a password
generatePassword()
// Restore password after rotation, otherwise generate a new one
if (savedInstanceState != null) {
passwordTextView.text = savedInstanceState.getString(KEY_PASSWORD, "")
} else {
generatePassword()
}
// Set click listener to copy password to clipboard
passwordTextView.setOnClickListener {
@ -46,98 +51,40 @@ class MainActivity : AppCompatActivity() {
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_PASSWORD, passwordTextView.text.toString())
}
private fun generatePassword() {
val passwordLength = passwordLengthPicker.value
val password = generateRandomWords(passwordLength)
passwordTextView.text = password
}
private fun generateRandomWords(numWords: Int): String {
val syllables = arrayOf(
"ing","er","a","ly","ed","i","es","re","tion","in","e","con","y","ter","ex","al","de","com",
"o","di","en","an","ty","ry","u","ti","ri","be","per","to","pro","ac","ad","ar","ers","ment",
"or","tions","ble","der","ma","na","si","un","at","dis","ca","cal","man","ap","po","sion","vi",
"el","est","la","lar","pa","ture","for","is","mer","pe","ra","so","ta","as","col","fi","ful",
"ger","low","ni","par","son","tle","day","ny","pen","pre","tive","car","ci","mo","on","ous",
"pi","se","ten","tor","ver","ber","can","dy","et","it","mu","no","ple","cu","fac","fer","gen",
"ic","land","light","ob","of","pos","tain","den","ings","mag","ments","set","some","sub","sur",
"ters","tu","af","au","cy","fa","im","li","lo","men","min","mon","op","out","rec","ro","sen",
"side","tal","tic","ties","ward","age","ba","but","cit","cle","co","cov","da","dif","ence",
"ern","eve","hap","ies","ket","lec","main","mar","mis","my","nal","ness","ning","n't","nu","oc",
"pres","sup","te","ted","tem","tin","tri","tro","up","va","ven","vis","am","bor","by","cat",
"cent","ev","gan","gle","head","high","il","lu","me","nore","part","por","read","rep","su",
"tend","ther","ton","try","um","uer","way","ate","bet","bles","bod","cap","cial","cir","cor",
"coun","cus","dan","dle","ef","end","ent","ered","fin","form","go","har","ish","lands","let",
"long","mat","meas","mem","mul","ner","play","ples","ply","port","press","sat","sec","ser",
"south","sun","the","ting","tra","tures","val","var","vid","wil","win","won","work","act","ag",
"air","als","bat","bi","cate","cen","char","come","cul","ders","east","fect","fish","fix","gi",
"grand","great","heav","ho","hunt","ion","its","jo","lat","lead","lect","lent","less","lin",
"mal","mi","mil","moth","near","nel","net","new","one","point","prac","ral","rect","ried",
"round","row","sa","sand","self","sent","ship","sim","sions","sis","sons","stand","sug","tel",
"tom","tors","tract","tray","us","vel","west","where","writing","er","i","y","ter","al","ed",
"es","e","tion","re","o","oth","ry","de","ver","ex","en","di","bout","com","ple","u","con",
"per","un","der","tle","ber","ty","num","peo","ble","af","ers","mer","wa","ment","pro","ar",
"ma","ri","sen","ture","fer","dif","pa","tions","ther","fore","est","fa","la","ei","not","si",
"ent","ven","ev","ac","ca","fol","ful","na","tain","ning","col","par","dis","ern","ny","cit",
"po","cal","mu","moth","pic","im","coun","mon","pe","lar","por","fi","bers","sec","ap","stud",
"ad","tween","gan","bod","tence","ward","hap","nev","ure","mem","ters","cov","ger","nit"
// Add more syllables as needed
)
val random = Random.Default
return buildString {
repeat(numWords) {
append(generateRandomWord(syllables, random))
append(PasswordGenerator.generateRandomWord())
append(" ") // Add space between words
}
}.let { password ->
// Check if the switch for adding special characters is checked
if (switchSpecialChars.isChecked) {
addSpecialCharacters(password)
PasswordGenerator.addSpecialCharacters(password)
} else {
password
}
}.trim() // Remove trailing space
}
private fun generateRandomWord(syllables: Array<String>, random: Random): String {
return buildString {
repeat(random.nextInt(2, 4)) {
append(syllables[random.nextInt(syllables.size)])
}
}
}
private fun copyToClipboard(text: String, clipboardManager: ClipboardManager) {
val clipData = if (switchWithoutSpaces.isChecked)
{ val textNoSpaces = text.filter { !it.isWhitespace() }
ClipData.newPlainText("Password", textNoSpaces)
val clipData = if (switchWithoutSpaces.isChecked) {
val textNoSpaces = text.filter { !it.isWhitespace() }
ClipData.newPlainText("Password", textNoSpaces)
} else {
ClipData.newPlainText("Password", text)
}
clipboardManager.setPrimaryClip(clipData)
}
private fun addSpecialCharacters(password: String): String {
// Insert special character, uppercase character, and digit at random positions within the password
val specialChars = listOf('$', ',', '.', '#', '@', '!', '%', '&')
val random = Random.Default
val index = random.nextInt(0, password.length + 1)
val specialChar = specialChars[random.nextInt(specialChars.size)]
val uppercaseChar = ('A'..'Z').random() // Generate random uppercase character
val digit = random.nextInt(0, 10) // Generate random digit (number)
val newPassword = StringBuilder(password)
newPassword.insert(index, specialChar)
newPassword.insert(random.nextInt(0, newPassword.length + 1), uppercaseChar)
newPassword.insert(random.nextInt(0, newPassword.length + 1), digit.toString())
//return password.substring(0, index) + specialChar + password.substring(index)
return newPassword.toString()
}
}

View File

@ -0,0 +1,64 @@
package cz.bugsy.passwordzebra
import kotlin.random.Random
internal object PasswordGenerator {
private val defaultRandom = Random.Default
private val syllables = arrayOf(
"ing","er","a","ly","ed","i","es","re","tion","in","e","con","y","ter","ex","al","de","com",
"o","di","en","an","ty","ry","u","ti","ri","be","per","to","pro","ac","ad","ar","ers","ment",
"or","tions","ble","der","ma","na","si","un","at","dis","ca","cal","man","ap","po","sion","vi",
"el","est","la","lar","pa","ture","for","is","mer","pe","ra","so","ta","as","col","fi","ful",
"ger","low","ni","par","son","tle","day","ny","pen","pre","tive","car","ci","mo","on","ous",
"pi","se","ten","tor","ver","ber","can","dy","et","it","mu","no","ple","cu","fac","fer","gen",
"ic","land","light","ob","of","pos","tain","den","ings","mag","ments","set","some","sub","sur",
"ters","tu","af","au","cy","fa","im","li","lo","men","min","mon","op","out","rec","ro","sen",
"side","tal","tic","ties","ward","age","ba","but","cit","cle","co","cov","da","dif","ence",
"ern","eve","hap","ies","ket","lec","main","mar","mis","my","nal","ness","ning","n't","nu","oc",
"pres","sup","te","ted","tem","tin","tri","tro","up","va","ven","vis","am","bor","by","cat",
"cent","ev","gan","gle","head","high","il","lu","me","nore","part","por","read","rep","su",
"tend","ther","ton","try","um","uer","way","ate","bet","bles","bod","cap","cial","cir","cor",
"coun","cus","dan","dle","ef","end","ent","ered","fin","form","go","har","ish","lands","let",
"long","mat","meas","mem","mul","ner","play","ples","ply","port","press","sat","sec","ser",
"south","sun","the","ting","tra","tures","val","var","vid","wil","win","won","work","act","ag",
"air","als","bat","bi","cate","cen","char","come","cul","ders","east","fect","fish","fix","gi",
"grand","great","heav","ho","hunt","ion","its","jo","lat","lead","lect","lent","less","lin",
"mal","mi","mil","moth","near","nel","net","new","one","point","prac","ral","rect","ried",
"round","row","sa","sand","self","sent","ship","sim","sions","sis","sons","stand","sug","tel",
"tom","tors","tract","tray","us","vel","west","where","writing","er","i","y","ter","al","ed",
"es","e","tion","re","o","oth","ry","de","ver","ex","en","di","bout","com","ple","u","con",
"per","un","der","tle","ber","ty","num","peo","ble","af","ers","mer","wa","ment","pro","ar",
"ma","ri","sen","ture","fer","dif","pa","tions","ther","fore","est","fa","la","ei","not","si",
"ent","ven","ev","ac","ca","fol","ful","na","tain","ning","col","par","dis","ern","ny","cit",
"po","cal","mu","moth","pic","im","coun","mon","pe","lar","por","fi","bers","sec","ap","stud",
"ad","tween","gan","bod","tence","ward","hap","nev","ure","mem","ters","cov","ger","nit"
// Add more syllables as needed
)
fun generateRandomWord(random: Random = defaultRandom): String {
return buildString {
repeat(random.nextInt(2, 4)) {
append(syllables[random.nextInt(syllables.size)])
}
}
}
fun addSpecialCharacters(password: String, random: Random = defaultRandom): String {
// Insert special character, uppercase character, and digit at random positions within the password
val specialChars = listOf('$', ',', '.', '#', '@', '!', '%', '&')
val index = random.nextInt(0, password.length + 1)
val specialChar = specialChars[random.nextInt(specialChars.size)]
val uppercaseChar = ('A'..'Z').random(random)
val digit = random.nextInt(0, 10)
val newPassword = StringBuilder(password)
newPassword.insert(index, specialChar)
newPassword.insert(random.nextInt(0, newPassword.length + 1), uppercaseChar)
newPassword.insert(random.nextInt(0, newPassword.length + 1), digit.toString())
return newPassword.toString()
}
}

View File

@ -2,11 +2,16 @@ package cz.bugsy.passwordzebra
import android.app.Application
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
class PasswordZebra : Application() {
override fun onCreate() {
// Apply dynamic color
//DynamicColors.applyToActivitiesIfAvailable(this)
DynamicColors.applyToActivitiesIfAvailable(this, R.style.AppTheme_Overlay)
DynamicColors.applyToActivitiesIfAvailable(
this,
DynamicColorsOptions.Builder()
.setThemeOverlay(R.style.AppTheme_Overlay)
.build()
)
}
}

View File

@ -4,8 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="2dp">
>
<TextView
android:id="@+id/passwordTextView"

View File

@ -4,8 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="2dp">
>
<TextView
android:id="@+id/passwordTextView"
@ -156,7 +155,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="364dp" />
app:layout_constraintGuide_percent="0.92" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
<!-- navigation bar = bottom bar with navigation buttons -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="colorPrimary">@color/md_theme_dark_primary</item>
@ -34,7 +34,7 @@
<item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
<item name="colorOnError">@color/md_theme_dark_onError</item>
<item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
<item name="android:colorBackground">@color/md_theme_dark_onBackground</item>
<item name="android:colorBackground">@color/md_theme_dark_background</item>
<item name="colorSurface">@color/md_theme_dark_surface</item>
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>

View File

@ -0,0 +1,86 @@
package cz.bugsy.passwordzebra
import org.junit.Assert.*
import org.junit.Test
import kotlin.random.Random
class PasswordGenerationTest {
// generateRandomWord tests
@Test
fun generateRandomWord_returnsNonEmptyString() {
val word = PasswordGenerator.generateRandomWord()
assertTrue(word.isNotEmpty())
}
@Test
fun generateRandomWord_consistsOnlyOfLowercaseLettersOrApostrophe() {
repeat(20) {
val word = PasswordGenerator.generateRandomWord()
assertTrue("Word '$word' contains unexpected characters",
word.all { it.isLowerCase() || it == '\'' })
}
}
@Test
fun generateRandomWord_withSameSeed_returnsSameResult() {
val word1 = PasswordGenerator.generateRandomWord(Random(42))
val word2 = PasswordGenerator.generateRandomWord(Random(42))
assertEquals(word1, word2)
}
@Test
fun generateRandomWord_withDifferentSeeds_canReturnDifferentResults() {
val words = (1..10).map { PasswordGenerator.generateRandomWord(Random(it.toLong())) }.toSet()
assertTrue("Expected multiple distinct words, got: $words", words.size > 1)
}
// addSpecialCharacters tests
@Test
fun addSpecialCharacters_addsExactlyThreeCharacters() {
val password = "hello world"
val result = PasswordGenerator.addSpecialCharacters(password)
assertEquals(password.length + 3, result.length)
}
@Test
fun addSpecialCharacters_containsAtLeastOneDigit() {
val password = "hello world"
val result = PasswordGenerator.addSpecialCharacters(password)
assertTrue("Expected a digit in '$result'", result.any { it.isDigit() })
}
@Test
fun addSpecialCharacters_containsAtLeastOneUppercaseLetter() {
val password = "hello world"
val result = PasswordGenerator.addSpecialCharacters(password)
assertTrue("Expected an uppercase letter in '$result'", result.any { it.isUpperCase() })
}
@Test
fun addSpecialCharacters_containsAtLeastOneSpecialCharacter() {
val specialChars = setOf('$', ',', '.', '#', '@', '!', '%', '&')
val password = "hello world"
val result = PasswordGenerator.addSpecialCharacters(password)
assertTrue("Expected a special character in '$result'", result.any { it in specialChars })
}
@Test
fun addSpecialCharacters_preservesOriginalCharsAsSubsequence() {
val password = "helloworld"
val result = PasswordGenerator.addSpecialCharacters(password)
var i = 0
result.forEach { c -> if (i < password.length && c == password[i]) i++ }
assertEquals("Original chars should remain as subsequence in '$result'", password.length, i)
}
@Test
fun addSpecialCharacters_withSameSeed_returnsSameResult() {
val password = "testpassword"
val result1 = PasswordGenerator.addSpecialCharacters(password, Random(99))
val result2 = PasswordGenerator.addSpecialCharacters(password, Random(99))
assertEquals(result1, result2)
}
}