From c2579f876505b236d82b26f26f186ecf93ba4078 Mon Sep 17 00:00:00 2001 From: pavelb Date: Mon, 2 Mar 2026 23:25:45 +0100 Subject: [PATCH] Fix code quality issues and refactor password generation --- .../cz/bugsy/passwordzebra/MainActivity.kt | 89 ++++--------------- .../bugsy/passwordzebra/PasswordGenerator.kt | 64 +++++++++++++ .../cz/bugsy/passwordzebra/PasswordZebra.kt | 9 +- .../main/res/layout-land/activity_main.xml | 3 +- app/src/main/res/layout/activity_main.xml | 5 +- app/src/main/res/values-night/themes.xml | 4 +- .../passwordzebra/PasswordGenerationTest.kt | 86 ++++++++++++++++++ 7 files changed, 180 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/cz/bugsy/passwordzebra/PasswordGenerator.kt create mode 100644 app/src/test/java/cz/bugsy/passwordzebra/PasswordGenerationTest.kt diff --git a/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt b/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt index 9c7f04e..4ec83e7 100644 --- a/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt +++ b/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt @@ -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, 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() - } } diff --git a/app/src/main/java/cz/bugsy/passwordzebra/PasswordGenerator.kt b/app/src/main/java/cz/bugsy/passwordzebra/PasswordGenerator.kt new file mode 100644 index 0000000..5b63c7e --- /dev/null +++ b/app/src/main/java/cz/bugsy/passwordzebra/PasswordGenerator.kt @@ -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() + } +} diff --git a/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt b/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt index 25ce221..a5d6b7b 100644 --- a/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt +++ b/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt @@ -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() + ) } } diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 5032e85..3556290 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -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"> +> +> + app:layout_constraintGuide_percent="0.92" /> diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index cfaccca..c1bac58 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -15,7 +15,7 @@ @android:color/transparent - true + false false @color/md_theme_dark_primary @@ -34,7 +34,7 @@ @color/md_theme_dark_errorContainer @color/md_theme_dark_onError @color/md_theme_dark_onErrorContainer - @color/md_theme_dark_onBackground + @color/md_theme_dark_background @color/md_theme_dark_surface @color/md_theme_dark_onSurface @color/md_theme_dark_surfaceVariant diff --git a/app/src/test/java/cz/bugsy/passwordzebra/PasswordGenerationTest.kt b/app/src/test/java/cz/bugsy/passwordzebra/PasswordGenerationTest.kt new file mode 100644 index 0000000..c4f94b5 --- /dev/null +++ b/app/src/test/java/cz/bugsy/passwordzebra/PasswordGenerationTest.kt @@ -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) + } +}