Fix code quality issues and refactor password generation
This commit is contained in:
parent
898f4aa262
commit
dff3db885d
@ -7,7 +7,8 @@ import android.os.Bundle
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlin.random.Random
|
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -37,8 +38,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
generatePassword()
|
generatePassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initially generate a password
|
// Restore password after rotation, otherwise generate a new one
|
||||||
generatePassword()
|
if (savedInstanceState != null) {
|
||||||
|
passwordTextView.text = savedInstanceState.getString(KEY_PASSWORD, "")
|
||||||
|
} else {
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
// Set click listener to copy password to clipboard
|
// Set click listener to copy password to clipboard
|
||||||
passwordTextView.setOnClickListener {
|
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() {
|
private fun generatePassword() {
|
||||||
val passwordLength = passwordLengthPicker.value
|
val passwordLength = passwordLengthPicker.value
|
||||||
val password = generateRandomWords(passwordLength)
|
val password = generateRandomWords(passwordLength)
|
||||||
|
|
||||||
passwordTextView.text = password
|
passwordTextView.text = password
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateRandomWords(numWords: Int): String {
|
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 {
|
return buildString {
|
||||||
repeat(numWords) {
|
repeat(numWords) {
|
||||||
append(generateRandomWord(syllables, random))
|
append(PasswordGenerator.generateRandomWord())
|
||||||
append(" ") // Add space between words
|
append(" ") // Add space between words
|
||||||
}
|
}
|
||||||
}.let { password ->
|
}.let { password ->
|
||||||
// Check if the switch for adding special characters is checked
|
// Check if the switch for adding special characters is checked
|
||||||
if (switchSpecialChars.isChecked) {
|
if (switchSpecialChars.isChecked) {
|
||||||
addSpecialCharacters(password)
|
PasswordGenerator.addSpecialCharacters(password)
|
||||||
} else {
|
} else {
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
}.trim() // Remove trailing space
|
}.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) {
|
private fun copyToClipboard(text: String, clipboardManager: ClipboardManager) {
|
||||||
|
val clipData = if (switchWithoutSpaces.isChecked) {
|
||||||
val clipData = if (switchWithoutSpaces.isChecked)
|
val textNoSpaces = text.filter { !it.isWhitespace() }
|
||||||
{ val textNoSpaces = text.filter { !it.isWhitespace() }
|
ClipData.newPlainText("Password", textNoSpaces)
|
||||||
ClipData.newPlainText("Password", textNoSpaces)
|
|
||||||
} else {
|
} else {
|
||||||
ClipData.newPlainText("Password", text)
|
ClipData.newPlainText("Password", text)
|
||||||
}
|
}
|
||||||
clipboardManager.setPrimaryClip(clipData)
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,16 @@ package cz.bugsy.passwordzebra
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
|
|
||||||
class PasswordZebra : Application() {
|
class PasswordZebra : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Apply dynamic color
|
// Apply dynamic color
|
||||||
//DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this, R.style.AppTheme_Overlay)
|
this,
|
||||||
|
DynamicColorsOptions.Builder()
|
||||||
|
.setThemeOverlay(R.style.AppTheme_Overlay)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity"
|
tools:context=".MainActivity"
|
||||||
tools:layout_editor_absoluteX="0dp"
|
>
|
||||||
tools:layout_editor_absoluteY="2dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/passwordTextView"
|
android:id="@+id/passwordTextView"
|
||||||
|
|||||||
@ -4,8 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity"
|
tools:context=".MainActivity"
|
||||||
tools:layout_editor_absoluteX="0dp"
|
>
|
||||||
tools:layout_editor_absoluteY="2dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/passwordTextView"
|
android:id="@+id/passwordTextView"
|
||||||
@ -156,7 +155,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintGuide_begin="364dp" />
|
app:layout_constraintGuide_percent="0.92" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<!-- navigation bar = bottom bar with navigation buttons -->
|
<!-- navigation bar = bottom bar with navigation buttons -->
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<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="android:windowTranslucentNavigation">false</item>
|
||||||
|
|
||||||
<item name="colorPrimary">@color/md_theme_dark_primary</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="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
|
||||||
<item name="colorOnError">@color/md_theme_dark_onError</item>
|
<item name="colorOnError">@color/md_theme_dark_onError</item>
|
||||||
<item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</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="colorSurface">@color/md_theme_dark_surface</item>
|
||||||
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
|
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
|
||||||
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
|
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user