diff --git a/README.md b/README.md index 34a330ab..ee86f1d8 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,61 @@ # Password Manager -A android app that take down the headace of remember passwords. It is open source solutions that help you in keep your passwords safe and secure in your own local storage without ever need to push you password to cloud. +A android app that take down the headace of remember passwords. It is open source solutions that help you in keep your passwords safe and secure in your own local storage without ever need to push them to cloud. > [!WARNING] -> It is just a open source project. An is under active development please, consider using it for fun and not for real password managment. (untill, we offically release a stable release) +> It is just a open source project. An is current under active development. +> Please, consider using it for fun and not for real password managment. (untill, we offically release a stable release) + +## Compatibility + +**Compile Sdk**: `Android 16 (API level 36)` + +``` +Android 8+ (Minimum) [API level 26+] +Android 14 [API level 34] (we support offically) + +Note: high version can still run but are not guaranteed offically. +``` ## Features - [X] Intuitive UI. -- [ ] Update Checkers. -- [X] Password Management. (current priority) -- [ ] Other Info Management. +- [ ] Update Checkers & Manager. +- [X] Password Management. (Current Priority) +- [ ] Secure File. (Least Priority, Because it include permission. Which, I am as developer not familar with 😂) + - Could be Image. (JPG. PNG ....) + - Could be Vidoe. + - Could any Binary File. (more like won't be a text file) +- [ ] Other Secret Info Management. + - Could be note. (txt file) + - Could be any info that can encode as key & value. + ```json + { + "key": "SECRET API KEY", + "content": "qwerty-let-say" + "createdat": "...", + "updatedat": "..." + } + ``` +- [ ] NON Secret Info Store +- [ ] Encryption and Decryption. - [ ] Backup Manager. - [ ] Import/Export Passwords. - [ ] Extensivity with custom database. +- [ ] Multiple Language Translation. +- [ ] Theme & Customization. +- [ ] Key Manager. +- [ ] Multi Platform Support. (KMP) ## Installation Steps 1. Go to our [github repository release page](https://github.com/JeelDobariya38/password-manager/releases/latest). + 2. Download the apk for your phone. If Don't know the architecture of phone then download apk file that has universal in its name. + 3. Install the apk and you are ready to use the app. -it was short and sweet description, for more detailed description vist the file [docs/installing.md](docs/installing.md) +It was most shortest and sweetest description (I have ever crafted), For more detailed description, Vist the file [docs/installing.md](docs/installing.md) ## Building The App @@ -33,7 +67,7 @@ it was short and sweet description, for more detailed description vist the file 4. For code documention and support docs, check the `docs/` folder in our repository. -it was short and sweet description, for more detailed description vist the file [docs/building.md](docs/building.md) +It was general, intuitive, short and sweet description, For more detailed description, Vist the file [docs/building.md](docs/building.md). ## Support Docs @@ -45,4 +79,4 @@ By, contribuating to project you accept the [CONTRIBUTING.md](CONTRIBUTING.md) & ## Licence -Password Manager Project is licence under [MIT](LICENSE.txt) Licence. Downloading the app would mean you are ok with the license. +Passcodes Project is licence under [MIT](LICENSE.txt) Licence. Downloading the app would mean, you are ok and have accepted the license. diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index c6418701..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,109 +0,0 @@ -plugins { - id 'com.android.application' -} - -android { - compileSdk 33 - namespace "com.passwordmanager" - - defaultConfig { - applicationId "com.passwordmanager" - minSdk 26 - targetSdk 33 - versionCode 1 - versionName "0.1.0-Alpha" - } - - signingConfigs { - release { - def keystorePropertiesFile = rootProject.file("keystore.properties") - if (keystorePropertiesFile.exists()) { - def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } - } - - splits { - abi { - enable true - reset() - include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk true - } - } - - lintOptions { - // You can enable lint checking and provide a baseline file if needed: - // baseline file("lint-baseline.xml") - - // To ensure that lint uses the lint.xml configuration - lintConfig rootProject.file("lint.xml") - } - - buildTypes { - release { - if (rootProject.file("keystore.properties").exists()) { - signingConfig signingConfigs.release - } - - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - manifestPlaceholders = [ - appIcon: "@mipmap/ic_launcher", - appLabel: "@string/app_name" - ] - } - - debug { - applicationIdSuffix ".dev" - versionNameSuffix "-Dev" - minifyEnabled false - - // the name come from a parent project name "PassCodes" - manifestPlaceholders = [ - appIcon: "@mipmap/dev_ic_launcher", - appLabel: "Passcodes Dev" - ] - } - - staging { - applicationIdSuffix ".staging" - versionNameSuffix "-Staging" - - minifyEnabled true - debuggable true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - manifestPlaceholders = [ - appIcon: "@mipmap/dev_ic_launcher", - appLabel: "Passcodes Stageing" - ] - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - viewBinding { - enabled = true - } -} - -dependencies { - implementation 'com.google.android.material:material:1.9.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.databinding:viewbinding:7.4.1' - implementation 'org.json:json:20250517' - - testImplementation 'junit:junit:4.13.2' - testImplementation "com.google.truth:truth:1.4.4" -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..ebc7b109 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,171 @@ +import java.io.FileInputStream +import java.util.Properties +import org.gradle.api.GradleException +import com.android.build.api.dsl.ApplicationExtension + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + // If you use Kotlin Parcelize, uncomment the next line: + // id("kotlin-parcelize") +} + +android { + (this as ApplicationExtension).apply { + compileSdk = 36 + namespace = "com.jeeldobariya.passcodes" + + defaultConfig { + applicationId = "com.jeeldobariya.passcodes" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0-Alpha" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } + } + + signingConfigs { + create("release") { + val keystorePropertiesFile = rootProject.file("keystore.properties") + if (keystorePropertiesFile.exists()) { + val keystoreProperties = Properties() + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } else { + logger.warn("WARNING: keystore.properties not found for release signing config.") + // throw GradleException("keystore.properties not found!") + } + } + } + + splits { + abi { + isEnable = true + reset() + include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + isUniversalApk = true + } + } + + lint { + // baseline = rootProject.file("lint-baseline.xml") // If you use a baseline + lintConfig = rootProject.file("lint.xml") + } + + buildTypes { + getByName("release") { + if (rootProject.file("keystore.properties").exists()) { + signingConfig = signingConfigs.getByName("release") + } else { + logger.warn("WARNING: Release build will not be signed as keystore.properties is missing.") + // throw GradleException("Can't Sign Release Build") + } + + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + + // Use manifestPlaceholders.put() for key-value pairs + manifestPlaceholders.put("appIcon", "@mipmap/ic_launcher") + manifestPlaceholders.put("appLabel", "@string/app_name") + } + + getByName("debug") { + applicationIdSuffix = ".dev" + versionNameSuffix = "-Dev" + isMinifyEnabled = false + + manifestPlaceholders.put("appIcon", "@mipmap/dev_ic_launcher") + manifestPlaceholders.put("appLabel", "Passcodes Dev") + } + + create("staging") { + applicationIdSuffix = ".staging" + versionNameSuffix = "-Staging" + + isMinifyEnabled = true + isShrinkResources = true + isDebuggable = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + + manifestPlaceholders.put("appIcon", "@mipmap/dev_ic_launcher") + manifestPlaceholders.put("appLabel", "Passcodes Staging") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + viewBinding = true + } + } +} + +dependencies { + val kotlinVersion = "1.9.0" + val materialVersion = "1.12.0" + val appCompatVersion = "1.7.0" + val roomVersion = "2.7.2" + // val jsonVersion = "20250517" + val junitVersion = "4.13.2" + val truthVersion = "1.4.4" + val androidxTestExtJunitVersion = "1.2.1" + val espressoCoreVersion = "3.6.1" + + val coroutinesVersion = "1.10.2" + val lifecycleVersion = "2.9.2" + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") + + implementation("com.google.android.material:material:$materialVersion") + implementation("androidx.appcompat:appcompat:$appCompatVersion") + // viewbinding is often not explicitly needed here if buildFeatures.viewBinding = true + // implementation("androidx.databinding:viewbinding:7.4.1") + + implementation("androidx.room:room-ktx:$roomVersion") + kapt("androidx.room:room-compiler:$roomVersion") + + // Kotlin Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + + // Lifecycle components for lifecycleScope + implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") + // implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") // Good for future ViewModels + + // // External JSON library + // implementation("org.json:json:$jsonVersion") // Standard org.json library + + // Testing dependencies + testImplementation("junit:junit:$junitVersion") + testImplementation("com.google.truth:truth:$truthVersion") + + // Room testing + androidTestImplementation("androidx.room:room-testing:$roomVersion") // Essential for Room testing + androidTestImplementation("androidx.test.ext:junit:$androidxTestExtJunitVersion") + androidTestImplementation("androidx.test.espresso:espresso-core:$espressoCoreVersion") + + // Coroutine test utilities (for runTest) + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + + // Assertion library (Google Truth) + androidTestImplementation("com.google.truth:truth:$truthVersion") +} diff --git a/app/schemas/com.jeeldobariya.passcodes.database.MasterDatabase/1.json b/app/schemas/com.jeeldobariya.passcodes.database.MasterDatabase/1.json new file mode 100644 index 00000000..0292e42d --- /dev/null +++ b/app/schemas/com.jeeldobariya.passcodes.database.MasterDatabase/1.json @@ -0,0 +1,67 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "14ca55af836200a4982f037d2dc57317", + "entities": [ + { + "tableName": "passwords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `notes` TEXT NOT NULL, `created_at` TEXT DEFAULT CURRENT_TIMESTAMP, `updated_at` TEXT DEFAULT CURRENT_TIMESTAMP)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "defaultValue": "CURRENT_TIMESTAMP" + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "defaultValue": "CURRENT_TIMESTAMP" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14ca55af836200a4982f037d2dc57317')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/jeeldobariya/passcodes/database/PasswordDatabaseTest.kt b/app/src/androidTest/kotlin/com/jeeldobariya/passcodes/database/PasswordDatabaseTest.kt new file mode 100644 index 00000000..94403a9a --- /dev/null +++ b/app/src/androidTest/kotlin/com/jeeldobariya/passcodes/database/PasswordDatabaseTest.kt @@ -0,0 +1,158 @@ +package com.jeeldobariya.passcodes.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +/** + * Instrumented test for the Room database, specifically testing the PasswordsDao. + * This test runs on an Android device/emulator. + */ +@RunWith(AndroidJUnit4::class) // Specifies the JUnit runner for Android instrumented tests +class PasswordDatabaseTest { + + private lateinit var passwordsDao: PasswordsDao + private lateinit var db: MasterDatabase + + // This function runs before each test method + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + // Build an in-memory database for testing. + // In-memory database ensures that tests are isolated and don't + // interfere with the actual app database or other tests. + db = Room.inMemoryDatabaseBuilder( + context, + MasterDatabase::class.java + ).allowMainThreadQueries() // Allow queries on the main thread for simplicity in tests + .build() + passwordsDao = db.passwordsDao // Get the DAO instance + } + + // This function runs after each test method + @After + @Throws(IOException::class) // Indicates that this method might throw an IOException + fun closeDb() { + db.close() // Close the database after each test to free resources + } + + @Test + fun insertAndGetAllPasswords_shouldReturnCorrectPasswords() = runTest { + // Test: Insert multiple passwords and then retrieve all to verify + val password1 = Password(domain = "example.com", username = "user1", password = "pass1", notes = "notes1") + val password2 = Password(domain = "test.org", username = "user2", password = "pass2", notes = "notes2") + + passwordsDao.insertPassword(password1) // Insert the first password + passwordsDao.insertPassword(password2) // Insert the second password + + // Collect the first emitted list from the Flow + val allPasswords = passwordsDao.getAllPasswords().first() + + // Assertions using Google Truth + assertThat(allPasswords).hasSize(2) // Check if two passwords were retrieved + assertThat(allPasswords[0].domain).isEqualTo("test.org") // Assuming DESC order by ID + assertThat(allPasswords[1].domain).isEqualTo("example.com") + assertThat(allPasswords).containsExactly(password2.copy(id = 2), password1.copy(id = 1)).inOrder(); + // Note: The 'id' is auto-generated by Room, so we need to create copies with the expected IDs for comparison. + // The order depends on your @Query("SELECT * FROM passwords ORDER BY id DESC") + } + + @Test + fun insertAndGetPasswordById_shouldReturnCorrectPassword() = runTest { + // Test: Insert a password and retrieve it by its auto-generated ID + val originalPassword = Password(domain = "domain.com", username = "user", password = "pass", notes = "some notes") + val insertedId = passwordsDao.insertPassword(originalPassword) // Insert and get the generated ID + + // Retrieve the password using the inserted ID + val retrievedPassword = passwordsDao.getPasswordById(insertedId.toInt()) + + // Assertions + assertThat(retrievedPassword).isNotNull() // Ensure a password was retrieved + assertThat(retrievedPassword?.id).isEqualTo(insertedId.toInt()) // Check if the ID matches + assertThat(retrievedPassword?.domain).isEqualTo(originalPassword.domain) // Check other properties + assertThat(retrievedPassword?.username).isEqualTo(originalPassword.username) + assertThat(retrievedPassword?.password).isEqualTo(originalPassword.password) + assertThat(retrievedPassword?.notes).isEqualTo(originalPassword.notes) + } + + @Test + fun insertAndGetPasswordByUsernameAndDomain_shouldReturnCorrectPassword() = runTest { + // Test: Insert a password and retrieve it by username and domain + val targetUsername = "specific_user" + val targetDomain = "specific_domain.net" + val passwordToFind = Password(domain = targetDomain, username = targetUsername, password = "pwd", notes = "find me") + val otherPassword = Password(domain = "other.net", username = "other", password = "xyz", notes = "not me") + + passwordsDao.insertPassword(otherPassword) + passwordsDao.insertPassword(passwordToFind) + + // Retrieve the password by username and domain + val retrievedPassword = passwordsDao.getPasswordByUsernameAndDomain(targetUsername, targetDomain) + + // Assertions + assertThat(retrievedPassword).isNotNull() + assertThat(retrievedPassword?.username).isEqualTo(targetUsername) + assertThat(retrievedPassword?.domain).isEqualTo(targetDomain) + } + + @Test + fun updatePassword_shouldUpdateCorrectly() = runTest { + // Test: Insert a password, update it, and verify the changes + val originalPassword = Password(domain = "old.com", username = "old_user", password = "old_pass", notes = "old_notes") + val insertedId = passwordsDao.insertPassword(originalPassword) + + // Create an updated password object (Room updates by primary key) + val updatedPassword = originalPassword.copy( + id = insertedId.toInt(), // Crucial: Set the ID of the existing row + domain = "new.com", + username = "new_user", + password = "new_pass", + notes = "new_notes" + ) + val rowsAffected = passwordsDao.updatePassword(updatedPassword) // Perform the update + + // Assert that one row was affected + assertThat(rowsAffected).isEqualTo(1) + + // Retrieve the password again and verify changes + val retrievedPassword = passwordsDao.getPasswordById(insertedId.toInt()) + assertThat(retrievedPassword).isNotNull() + assertThat(retrievedPassword?.domain).isEqualTo("new.com") + assertThat(retrievedPassword?.username).isEqualTo("new_user") + assertThat(retrievedPassword?.password).isEqualTo("new_pass") + assertThat(retrievedPassword?.notes).isEqualTo("new_notes") + } + + @Test + fun deletePasswordById_shouldDeleteCorrectly() = runTest { + // Test: Insert a password, delete it by ID, and verify its absence + val passwordToDelete = Password(domain = "delete.me", username = "trash", password = "123", notes = "delete this") + val insertedId = passwordsDao.insertPassword(passwordToDelete) + + // Delete the password by its ID + val rowsDeleted = passwordsDao.deletePasswordById(insertedId.toInt()) + + // Assert that one row was deleted + assertThat(rowsDeleted).isEqualTo(1) + + // Try to retrieve the deleted password; it should be null + val retrievedPassword = passwordsDao.getPasswordById(insertedId.toInt()) + assertThat(retrievedPassword).isNull() + } + + @Test + fun getAllPasswords_whenEmpty_shouldReturnEmptyList() = runTest { + // Test: Verify that getAllPasswords returns an empty list when the database is empty + val allPasswords = passwordsDao.getAllPasswords().first() + assertThat(allPasswords).isEmpty() // Assert that the list is empty + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c100584f..fc631e7e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ + android:targetSdkVersion="34" /> @@ -34,7 +34,7 @@ View Changelog - Password Manager + Passcodes Load Password Save Password View Password @@ -62,6 +62,7 @@ Success: Updated Successfully!! Deleted Successfully!! + Something Went Wrong: Invalid ID!! Something Went Wrong!! \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index eafb2406..c316e556 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ -