diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d148f636..3e1d300f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,6 +127,7 @@ android { buildFeatures { viewBinding = true + buildConfig = true } } @@ -150,6 +151,9 @@ dependencies { implementation(libs.room.ktx) ksp(libs.room.compiler) + implementation(libs.okhttp) + implementation(libs.json) + implementation(libs.coroutines.core) implementation(libs.coroutines.android) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eaa3fe1b..21615d35 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + - lifecycleScope.launch { controller.clearAllData() } + lifecycleScope.launch { + controller.clearAllData() + } + + Toast.makeText(this@SettingsActivity, "Delete the user data!!", Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Constants.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Constants.kt index 5dc959bd..13be42ff 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Constants.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Constants.kt @@ -1,12 +1,15 @@ package com.jeeldobariya.passcodes.utils object Constant { - // Url Constants - const val REPO_URL = "https://github.com/JeelDobariya38/Passcodes" - const val REPORT_BUG_URL = "https://github.com/JeelDobariya38/password-manager/issues/new?template=bug-report.md" - const val RELEASE_NOTE_URL = "https://github.com/JeelDobariya38/Passcodes/blob/main/docs/release-notes.md" - const val SECURITY_GUIDE_URL = "https://github.com/JeelDobariya38/Passcodes/blob/main/docs/security-guide.md" + const val REPOSITORY_SIGNATURE = "JeelDobariya38/Passcodes" + + // URL Constants const val TELEGRAM_COMMUNITY_URL = "https://t.me/passcodescommunity" + const val REPOSITORY_URL = "https://github.com/$REPOSITORY_SIGNATURE" + const val GITHUB_RELEASE_API_URL = "https://api.github.com/repos/$REPOSITORY_SIGNATURE/releases" + const val REPORT_BUG_URL = "$REPOSITORY_URL/issues/new?template=bug-report.md" + const val RELEASE_NOTE_URL = "$REPOSITORY_URL/blob/main/docs/release-notes.md" + const val SECURITY_GUIDE_URL = "$REPOSITORY_URL/blob/main/docs/security-guide.md" // Shared Preferences Constants diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt index e264fb71..84fd0c9b 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt @@ -1,6 +1,7 @@ package com.jeeldobariya.passcodes.utils import android.content.Context +import android.widget.Toast import com.jeeldobariya.passcodes.database.MasterDatabase import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.database.PasswordsDao @@ -22,7 +23,7 @@ class Controller(context: Context) { } companion object { - const val CSV_HEADER = "name,url,username,password,notes" + const val CSV_HEADER = "name,url,username,password,note" } /** @@ -149,20 +150,30 @@ class Controller(context: Context) { return CSV_HEADER + "\n" + rows } - suspend fun importDataFromCsvString(csvString: String): Int { + suspend fun importDataFromCsvString(csvString: String): IntArray { val lines = csvString.lines().filter { it.isNotBlank() } - if (lines.isEmpty() || lines[0] != CSV_HEADER) { - throw InvalidImportFormat() + if (lines.isEmpty()) { + throw InvalidImportFormat("Given data seems to be Empty!!") + } + + if (lines[0] != CSV_HEADER) { + throw InvalidImportFormat("Given data is not in valid csv format!! correct format:- ${CSV_HEADER}") } var importedPasswordCount = 0 + var failToImportedPasswordCount = 0 lines.drop(1).forEach { line -> val cols = line.split(",") + /* NOTE: this need to be done, because our app not allow empty domain. */ + val chosenDomain : String = if (!cols[0].isBlank()) { + cols[0].trim() // used: name + } else cols[1].trim() // used: url + try { - val password: Password? = passwordsDao.getPasswordByUsernameAndDomain(username = cols[2].trim(), domain = cols[0].trim()) + val password: Password? = passwordsDao.getPasswordByUsernameAndDomain(username = cols[2].trim(), domain = chosenDomain) if (password != null) { updatePassword( @@ -174,7 +185,7 @@ class Controller(context: Context) { ) } else { savePasswordEntity( - domain = cols[0].trim(), + domain = chosenDomain, username = cols[2].trim(), password = cols[3].trim(), notes = cols[4].trim() @@ -184,9 +195,10 @@ class Controller(context: Context) { importedPasswordCount++ } catch (e: InvalidInputException) { e.printStackTrace() + failToImportedPasswordCount++ } } - return importedPasswordCount + return intArrayOf(importedPasswordCount, failToImportedPasswordCount) } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCode.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCode.kt deleted file mode 100644 index 91b65ad9..00000000 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCode.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.jeeldobariya.passcodes.utils - -class ExampleTestableCode { - fun checkStrength(password: String): Int { - if (password.isEmpty()) { - return -1; - } - - val length = password.length; - - if (length < 8) { - return 0; - } else { - return 1; - } - } -} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtils.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtils.kt new file mode 100644 index 00000000..c323dba9 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtils.kt @@ -0,0 +1,59 @@ +package com.jeeldobariya.passcodes.utils + +import org.json.JSONArray + +object SemVerUtils { + /** Compare two semantic versions. + * > 0 if v1 > v2, < 0 if v1 < v2, 0 if equal + */ + fun compare(v1: String, v2: String): Int { + val cleanV1 = v1.trimStart('v', 'V') + val cleanV2 = v2.trimStart('v', 'V') + + val parts1 = cleanV1.split(".") + val parts2 = cleanV2.split(".") + + val maxLength = maxOf(parts1.size, parts2.size) + for (i in 0 until maxLength) { + val p1 = parts1.getOrNull(i)?.toIntOrNull() ?: 0 + val p2 = parts2.getOrNull(i)?.toIntOrNull() ?: 0 + if (p1 != p2) return p1 - p2 + } + return 0 + } + + data class Release( + val tag: String, + val prerelease: Boolean, + val draft: Boolean + ) + + fun parseReleases(json: String): List { + val array = JSONArray(json) + val list = mutableListOf() + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + list.add( + Release( + tag = obj.getString("tag_name"), + prerelease = obj.getBoolean("prerelease"), + draft = obj.getBoolean("draft") + ) + ) + } + return list + } + + fun normalize(tag: String): String { + // Remove 'v' prefix if present + val clean = tag.trimStart('v', 'V') + + // Cut off pre-release (-...) or build metadata (+...) + val cutIndex = clean.indexOfAny(charArrayOf('-', '+')) + return if (cutIndex != -1) { + "v" + clean.substring(0, cutIndex) + } else { + "v$clean" + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt new file mode 100644 index 00000000..37090715 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt @@ -0,0 +1,71 @@ +package com.jeeldobariya.passcodes.utils + +import android.content.Context +import android.widget.Toast +import okhttp3.* +import java.io.IOException + +object UpdateChecker { + private val client = OkHttpClient() + + fun checkVersion(context: Context, currentVersion: String) { + val appcontext = context.applicationContext + val currentNormalizeVersion = SemVerUtils.normalize(currentVersion) + + val request = Request.Builder() + .url(Constant.GITHUB_RELEASE_API_URL) + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + e.printStackTrace() + } + + override fun onResponse(call: Call, response: Response) { + val body = response.body?.string() ?: return + val releases = SemVerUtils.parseReleases(body) + + var userReleaseFound = false + var latestStable: String? = null + + for (release in releases) { + if (release.draft) continue // ignore drafts + + if (release.tag == currentNormalizeVersion) { + userReleaseFound = true + if (release.prerelease) { + showToast( + appcontext, + "⚠️ You are using a PRE-RELEASE ($currentNormalizeVersion). Not safe for use! Join telegram community (${Constant.TELEGRAM_COMMUNITY_URL})" + ) + } + } + + if (!release.prerelease) { + if (latestStable == null || + SemVerUtils.compare(release.tag, latestStable) > 0 + ) { + latestStable = release.tag + } + } + } + + latestStable?.let { + if (SemVerUtils.compare(currentNormalizeVersion, it) < 0) { + showToast(appcontext, "New Update available: $it... Vist our website...") + } + } + + if (!userReleaseFound) { + showToast(appcontext, "⚠️ Version ($currentNormalizeVersion) not found on GitHub releases... Join telegram community (${Constant.TELEGRAM_COMMUNITY_URL})") + } + } + }) + } + + private fun showToast(context: Context, message: String) { + android.os.Handler(context.mainLooper).post { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index c0daf6eb..928a096e 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -136,12 +136,11 @@ diff --git a/app/src/main/res/layout/activity_view_password.xml b/app/src/main/res/layout/activity_view_password.xml index 272b3a8b..17f7650e 100644 --- a/app/src/main/res/layout/activity_view_password.xml +++ b/app/src/main/res/layout/activity_view_password.xml @@ -82,7 +82,7 @@ Load Password Update Password Delete Password - Import Password - Export Password - Clear All Data + Import G-Password + Export G-Password + Clear Data Check Security Settings Toggle Theme @@ -67,7 +67,7 @@ Password: Notes: Created At: - Updated At: + Last Updated: Permission granted @@ -87,7 +87,7 @@ Action discarded. Something Went Wrong: Invalid ID!! Imported %1$d passwords - Failed to import CSV + Failed to import %1$d passwords Passwords exported diff --git a/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCodeTest.kt b/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCodeTest.kt deleted file mode 100644 index 95cc7d62..00000000 --- a/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/ExampleTestableCodeTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.jeeldobariya.passcodes.utils - -import org.junit.Test -import org.junit.Before -import com.google.common.truth.Truth.assertThat - -class ExampleTestableCodeTest { - private lateinit var testObj: ExampleTestableCode - - @Before - fun setup() { - testObj = ExampleTestableCode() - } - - @Test - fun `should handle empty passwords`() { - // Given - val password = "" - - // When & Then - assertThat(testObj.checkStrength(password)).isEqualTo(-1) - } - - @Test - fun `should detect short password as weak`() { - // Given - val password = "short" // Less than 8 characters - - // When & Then - assertThat(testObj.checkStrength(password)).isEqualTo(0) - } - - @Test - fun `should detect log password as weak`() { - // Given - val password = "long password" // More than 8 characters - - // When & Then - assertThat(testObj.checkStrength(password)).isEqualTo(1) - } -} diff --git a/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtilsTest.kt b/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtilsTest.kt new file mode 100644 index 00000000..2ce73484 --- /dev/null +++ b/app/src/test/kotlin/com/jeeldobariya/passcodes/utils/SemVerUtilsTest.kt @@ -0,0 +1,111 @@ +package com.jeeldobariya.passcodes.utils + +import org.junit.Test +import com.google.common.truth.Truth.assertThat + +class SemVerUtilsTest { + + @Test + fun testCompareEqual() { + assertThat(SemVerUtils.compare("1.2.3", "1.2.3")).isEqualTo(0) + assertThat(SemVerUtils.compare("v1.0.0", "v1.0.0")).isEqualTo(0) + } + + @Test + fun testCompareGreater() { + assertThat(SemVerUtils.compare("1.2.10", "1.2.2")).isGreaterThan(0) + assertThat(SemVerUtils.compare("v2.0.0", "v1.9.9")).isGreaterThan(0) + } + + @Test + fun testCompareLess() { + assertThat(SemVerUtils.compare("1.2.2", "v1.2.10")).isLessThan(0) + assertThat(SemVerUtils.compare("v0.9.0", "1.0.0")).isLessThan(0) + } + + @Test + fun testPrefixV() { + assertThat(SemVerUtils.compare("v1.2.10", "v1.2.2")).isGreaterThan(0) + assertThat(SemVerUtils.compare("V2.0.0", "V1.9.9")).isGreaterThan(0) + assertThat(SemVerUtils.compare("1.2.2", "v1.2.10")).isLessThan(0) + assertThat(SemVerUtils.compare("v0.9.0", "1.0.0")).isLessThan(0) + } + + @Test + fun testNormalize() { + assertThat(SemVerUtils.normalize("v1.2.3-beta")).isEqualTo("v1.2.3") + assertThat(SemVerUtils.normalize("1.0.0-alpha.1+001")).isEqualTo("v1.0.0") + assertThat(SemVerUtils.normalize("V2.3.4-rc1")).isEqualTo("v2.3.4") + assertThat(SemVerUtils.normalize("1.5.0")).isEqualTo("v1.5.0") + assertThat(SemVerUtils.normalize("v2.5.0")).isEqualTo("v2.5.0") + assertThat(SemVerUtils.normalize("v1.0.0-Stable-Dev")).isEqualTo("v1.0.0") + + // Note: try some invalid / wired stuff just to test + assertThat(SemVerUtils.normalize("v1.0-Stable-Dev")).isEqualTo("v1.0") + assertThat(SemVerUtils.normalize("v1.0------")).isEqualTo("v1.0") + assertThat(SemVerUtils.normalize("v1.0-abc")).isEqualTo("v1.0") + assertThat(SemVerUtils.normalize("v--1.0-abc")).isEqualTo("v") + assertThat(SemVerUtils.normalize("")).isEqualTo("v") + } + + @Test + fun testParseReleases() { + val json = """ + [ + { + "url": "https://api.github.com/repos/JeelDobariya38/Passcodes/releases/240385777", + "assets_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/releases/240385777/assets", + "upload_url": "https://uploads.github.com/repos/JeelDobariya38/Passcodes/releases/240385777/assets{?name,label}", + "html_url": "https://github.com/JeelDobariya38/Passcodes/releases/tag/v1.0.0", + "id": 240385777, + "author": {}, + "node_id": "RE_kwDOMffp084OU_7x", + "tag_name": "v1.0.0", + "target_commitish": "main", + "name": "v1.0.0 - Stable Release", + "draft": false, + "immutable": false, + "prerelease": false, + "created_at": "2025-08-16T17:23:16Z", + "updated_at": "2025-08-17T12:56:19Z", + "published_at": "2025-08-16T18:29:16Z", + "assets": [], + "tarball_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/tarball/v1.0.0", + "zipball_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/zipball/v1.0.0", + "body": "", + "mentions_count": 3 + }, + { + "url": "https://api.github.com/repos/JeelDobariya38/Passcodes/releases/171838408", + "assets_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/releases/171838408/assets", + "upload_url": "https://uploads.github.com/repos/JeelDobariya38/Passcodes/releases/171838408/assets{?name,label}", + "html_url": "https://github.com/JeelDobariya38/Passcodes/releases/tag/v0.1.0", + "id": 171838408, + "author": {}, + "node_id": "RE_kwDOMffp084KPgvI", + "tag_name": "v0.1.0", + "target_commitish": "main", + "name": "v0.1.0 - Alpha Release [Yanked Released]", + "draft": false, + "immutable": false, + "prerelease": true, + "created_at": "2024-08-25T16:13:32Z", + "updated_at": "2025-08-16T16:09:32Z", + "published_at": "2024-08-26T03:51:02Z", + "assets": [], + "tarball_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/tarball/v0.1.0", + "zipball_url": "https://api.github.com/repos/JeelDobariya38/Passcodes/zipball/v0.1.0", + "body": "", + "mentions_count": 2 + } + ] + """.trimIndent() + + val releases = SemVerUtils.parseReleases(json) + + assertThat(releases.size).isEqualTo(2) + assertThat(releases[0].tag).isEqualTo("v1.0.0") + assertThat(releases[0].prerelease).isFalse() + assertThat(releases[1].prerelease).isTrue() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01765b0b..94c75d95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,11 @@ [versions] kotlin = "1.9.0" material = "1.12.0" +okhttp = "5.1.0" oss-license = "17.2.1" appcompat = "1.7.0" room = "2.7.2" -# json = "20250517" +json = "20250517" junit = "4.13.2" truth = "1.4.4" androidx-test-ext-junit = "1.2.1" @@ -29,6 +30,8 @@ room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp"} + coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -36,7 +39,7 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } # lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } -# json = { module = "org.json:json", version.ref = "json" } +json = { module = "org.json:json", version.ref = "json" } junit = { module = "junit:junit", version.ref = "junit" } truth = { module = "com.google.truth:truth", version.ref = "truth" }