diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml deleted file mode 100644 index 0880129c..00000000 --- a/.github/workflows/dependency-check.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Gradle Dependency Submission - -on: - push: - branches: [ 'main' ] - workflow_dispatch: # This makes it a manual trigger - -permissions: - contents: write - -jobs: - dependency-submission: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v4 - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@v4 diff --git a/app/build.gradle b/app/build.gradle index 506ff222..c6418701 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,28 +14,13 @@ android { versionName "0.1.0-Alpha" } - flavorDimensions "default" - - productFlavors { - dev { - dimension "default" - applicationIdSuffix ".dev" - versionNameSuffix "-Dev" - manifestPlaceholders = [appIcon: "@mipmap/dev_ic_launcher", appLabel: "Devpasscode"] // the name come from a parent project name "PassCodes" - } - prod { - dimension "default" - manifestPlaceholders = [appIcon: "@mipmap/ic_launcher", appLabel: "@string/app_name"] - } - } - 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']) @@ -43,7 +28,7 @@ android { } } } - + splits { abi { enable true @@ -66,9 +51,40 @@ android { 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" + ] } } @@ -76,7 +92,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - + viewBinding { enabled = true } @@ -86,4 +102,8 @@ 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/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bc04022..c100584f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,13 +1,12 @@ + xmlns:tools="http://schemas.android.com/tools"> - + + diff --git a/app/src/main/java/com/passwordmanager/ui/MainActivity.java b/app/src/main/java/com/passwordmanager/ui/MainActivity.java index cfd6e1e2..4a7dbf47 100644 --- a/app/src/main/java/com/passwordmanager/ui/MainActivity.java +++ b/app/src/main/java/com/passwordmanager/ui/MainActivity.java @@ -52,18 +52,13 @@ public void onRequestPermissionsResult( // Added all the onclick event listiners private void addOnClickListenerOnButton(ActivityMainBinding binding) { - binding.savePasswordBtn.setOnClickListener(v -> { - Intent savepasswordintent = new Intent(MainActivity.this, SavePasswordActivity.class); - startActivity(savepasswordintent); + binding.passwordManagerBtn.setOnClickListener(v -> { + Intent passwordmanagerintent = new Intent(MainActivity.this, PasswordManagerActivity.class); + startActivity(passwordmanagerintent); }); - binding.loadPasswordBtn.setOnClickListener(v -> { - Intent loadpasswordintent = new Intent(MainActivity.this, LoadPasswordActivity.class); - startActivity(loadpasswordintent); - }); - - binding.aboutUsBtn.setOnClickListener(v -> { - Intent aboutusintent = new Intent(MainActivity.this, AboutUsActivity.class); + binding.aboutUsBtn.setOnClickListener(v -> { + Intent aboutusintent = new Intent(MainActivity.this, AboutUsActivity.class); startActivity(aboutusintent); }); diff --git a/app/src/main/java/com/passwordmanager/ui/PasswordManagerActivity.java b/app/src/main/java/com/passwordmanager/ui/PasswordManagerActivity.java new file mode 100644 index 00000000..cc761464 --- /dev/null +++ b/app/src/main/java/com/passwordmanager/ui/PasswordManagerActivity.java @@ -0,0 +1,138 @@ +package com.passwordmanager.ui; + +import android.net.Uri; +import android.os.Bundle; +import android.provider.DocumentsContract; +import android.widget.Toast; +import androidx.annotation.Nullable; +import android.content.Intent; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import android.view.LayoutInflater; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.passwordmanager.R; +import com.passwordmanager.utils.Controller; +import com.passwordmanager.models.PasswordModel; +import com.passwordmanager.databinding.ActivityPasswordManagerBinding; + +import java.util.List; +import java.io.OutputStream; + +public class PasswordManagerActivity extends AppCompatActivity { + private Controller controller; + private static final int CREATE_EXPORT_DATA_FILE_REQUEST = 1; + + private String exportPasswordsContent; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ActivityPasswordManagerBinding binding = ActivityPasswordManagerBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + // Add event onclick listener + addOnClickListenerOnButton(binding); + + // Make window fullscreen + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + } + + // Added all the onclick event listiners + private void addOnClickListenerOnButton(ActivityPasswordManagerBinding binding) { + binding.savePasswordBtn.setOnClickListener(v -> { + Intent savepasswordintent = new Intent(PasswordManagerActivity.this, SavePasswordActivity.class); + startActivity(savepasswordintent); + }); + + binding.loadPasswordBtn.setOnClickListener(v -> { + Intent loadpasswordintent = new Intent(PasswordManagerActivity.this, LoadPasswordActivity.class); + startActivity(loadpasswordintent); + }); + + binding.securityCheckBtn.setOnClickListener(v -> { + Toast.makeText(this, getString(R.string.future_feat_clause), Toast.LENGTH_SHORT).show(); + }); + + binding.importPasswordBtn.setOnClickListener(v -> { + Toast.makeText(this, getString(R.string.future_feat_clause), Toast.LENGTH_SHORT).show(); + }); + + binding.exportPasswordBtn.setOnClickListener(v -> { + controller = new Controller(PasswordManagerActivity.this); + List allPasswords = controller.getAllPasswords(); + exportPasswordsContent = convertPasswordsToJson(allPasswords); + + if (exportPasswordsContent != null) { + createFile(null); + } else { + Toast.makeText(this, "Failed to generate secure report content.", Toast.LENGTH_SHORT).show(); + } + }); + } + + private String convertPasswordsToJson(List passwordList) { + JSONArray jsonArray = new JSONArray(); + try { + for (PasswordModel password : passwordList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", password.getId()); + jsonObject.put("domain", password.getDomain()); + jsonObject.put("username", password.getUsername()); + jsonObject.put("password", password.getPassword()); // !!! Highly Sensitive Data !!! + jsonObject.put("notes", password.getNotes()); + jsonObject.put("createdAt", password.getCreatedAt()); + jsonObject.put("updatedAt", password.getUpdatedAt()); + jsonArray.put(jsonObject); + } + + return jsonArray.toString(4); + } catch (JSONException e) { + return null; + } + } + + private void createFile(@Nullable Uri pickerInitialUri) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/json"); + intent.putExtra(Intent.EXTRA_TITLE, "password_manager_data.json"); + + if (pickerInitialUri != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); + } + + startActivityForResult(intent, CREATE_EXPORT_DATA_FILE_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == CREATE_EXPORT_DATA_FILE_REQUEST) { + if (resultCode == RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri != null && exportPasswordsContent != null) { + try { + OutputStream outputStream = getContentResolver().openOutputStream(uri); + if (outputStream != null) { + outputStream.write(exportPasswordsContent.getBytes()); + outputStream.close(); + Toast.makeText(this, "Data export successfully to: " + uri.getPath(), Toast.LENGTH_LONG).show(); + } + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "Error export data: " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } else { + Toast.makeText(this, "Failed to get file URI or report content is empty.", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(this, "File creation cancelled.", Toast.LENGTH_SHORT).show(); + } + } + } +} diff --git a/app/src/main/java/com/passwordmanager/utils/ExampleTestableCode.java b/app/src/main/java/com/passwordmanager/utils/ExampleTestableCode.java new file mode 100644 index 00000000..32c57ae4 --- /dev/null +++ b/app/src/main/java/com/passwordmanager/utils/ExampleTestableCode.java @@ -0,0 +1,17 @@ +package com.passwordmanager.utils; + +class ExampleTestableCode { + int checkStrength(String password) { + if (password == null || password.isEmpty()) { + return -1; + } + + int length = password.length(); + + if (length < 8) { + return 0; + } else { + return 1; + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3768668e..18e09e6f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,70 +1,63 @@ - - - - - - - - - - - + tools:context=".ui.MainActivity" + android:padding="4sp" > + + - - - - - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_password_manager.xml b/app/src/main/res/layout/activity_password_manager.xml new file mode 100644 index 00000000..848650a5 --- /dev/null +++ b/app/src/main/res/layout/activity_password_manager.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec970ff1..1d359288 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,16 +20,21 @@ Username Password Notes + Password Manager Save Password Load Password Update Password Delete Password + Import Password + Export Password + Check Security About Us View License View Security Guidelines View Changelog + Password Manager Load Password Save Password View Password @@ -50,6 +55,7 @@ Permission Denied + Feature is under development!! 404: Not Found!! Warning: please fill the form first!! Failed: please try again!! diff --git a/app/src/test/java/com/passwordmanager/utils/ExampleTestableCodeTest.java b/app/src/test/java/com/passwordmanager/utils/ExampleTestableCodeTest.java new file mode 100644 index 00000000..f983607b --- /dev/null +++ b/app/src/test/java/com/passwordmanager/utils/ExampleTestableCodeTest.java @@ -0,0 +1,41 @@ +package com.passwordmanager.utils; + +import org.junit.Test; +import org.junit.Before; +import static com.google.common.truth.Truth.assertThat; + +public class ExampleTestableCodeTest { + private ExampleTestableCode testObj; + + @Before + public void setup() { + testObj = new ExampleTestableCode(); + } + + @Test + public void testCheckStrength_With_EmptyPassword() { + // Given + String password = ""; + + // When & Then + assertThat(testObj.checkStrength(password)).isEqualTo(-1); + } + + @Test + public void testCheckStrength_Weak_ShortPassword() { + // Given + String password = "short"; // Less than 8 characters + + // When & Then + assertThat(testObj.checkStrength(password)).isEqualTo(0); + } + + @Test + public void testCheckStrength_Strong_LongPassword() { + // Given + String password = "long password"; // More than 8 characters + + // When & Then + assertThat(testObj.checkStrength(password)).isEqualTo(1); + } +} diff --git a/docs/building.md b/docs/building.md index e0e73bc8..9cfcf86c 100644 --- a/docs/building.md +++ b/docs/building.md @@ -10,20 +10,20 @@ You will need `gradle` and `adb` accessible from commandline so, that script can something like this... ``` -PS C:\Users\HP> gradle help +PS C:\Users\ABC> gradle help > Task :help Welcome to Gradle 8.9. -Directory 'C:\Users\HP' does not contain a Gradle build. +Directory 'C:\Users\ABC' does not contain a Gradle build. .... BUILD SUCCESSFUL in 1s 1 actionable task: 1 executed -PS C:\Users\HP> adb help +PS C:\Users\ABC> adb help Android Debug Bridge version 1.0.41 Version 35.0.2-12147458 Installed as C:...\cmdline-tools\lib\platform-tools\adb.exe diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 00000000..99c9f122 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,49 @@ +# Important Commads + +- For Building A Clean ALL Variant + +```bash +./gradlew clean assemble +``` + +- For Building A Clean Release Variant + +```bash +./gradlew clean assembleRelease +``` + +- For Building A Staging Variant + +```bash +./gradlew assembleStaging +``` + +- For Building A Debug Variant + +```bash +./gradlew assembleDebug +``` + +- For Clearing Build Files + +```bash +./gradlew clean +``` + +- For sync depeendency + +```bash +./gradlew --refresh-dependencies +``` + +- For test app + +```bash +./gradlew testDebugUnitTest +``` + +- For install app on usb connect device + +```bash +./gradlew installDebug +``` diff --git a/git-hooks/commit-msg.sample b/git-hooks/commit-msg.sample new file mode 100644 index 00000000..d94cc1ef --- /dev/null +++ b/git-hooks/commit-msg.sample @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# Path to the commit message file (provided by Git). +COMMIT_MSG_FILE=$1 + +# Ignore automatic commit messages containing ' into ' or 'Merge'. +if grep --quiet --extended-regexp " into |^Merge " "$COMMIT_MSG_FILE"; then + exit 0 +fi + +# Read the commit message from the file. +COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") + +CONVENTIONAL_COMMIT_REGEX='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([a-zA-Z0-9_.-]+\))?(!)?:\s.*$' + +# Check if the commit message matches the regex. +if ! [[ $COMMIT_MSG =~ $CONVENTIONAL_COMMIT_REGEX ]]; then + echo "ERROR: Commit message does not follow Conventional Commits format." + echo + echo "The commit message should be structured as follows:" + echo "(): " + echo "[optional body]" + echo "[optional footer(s)]" + echo + echo "Valid types are:" + echo " feat: A new feature." + echo " fix: A bug fix." + echo " docs: Documentation changes." + echo " style: Code style changes (formatting, missing semicolons, etc.)." + echo " refactor: Code refactoring (neither fixes a bug nor adds a feature)." + echo " test: Adding or updating tests." + echo " chore: Routine tasks like updating dependencies or build tools." + echo " build: Changes affecting the build system or external dependencies." + echo " ci: Changes to CI configuration files or scripts." + echo " perf: Performance improvements." + echo " revert: Reverting a previous commit." + echo + echo "Examples:" + echo " feat(auth): add login functionality" + echo " fix(api)!: resolve timeout issue" + echo " docs(readme): update installation instructions" + echo + exit 1 +fi + +exit 0 diff --git a/installondevice.bat b/installondevice.bat index 637b619f..5e97807d 100644 --- a/installondevice.bat +++ b/installondevice.bat @@ -3,14 +3,14 @@ @REM Force continuation after Gradle command by capturing the output if "%~1"=="prod" ( echo building a production build - call gradlew clean assembleProdRelease + call gradlew clean assembleRelease if ERRORLEVEL 1 ( echo Gradle build failed! Check build_output.txt for details. exit /b 1 ) ) else ( echo building a development build - call gradlew assembleDevDebug + call gradlew assembleDebug if ERRORLEVEL 1 ( echo Gradle build failed! Check build_output.txt for details. exit /b 1 @@ -35,14 +35,14 @@ if ERRORLEVEL 1 ( @REM Install the APK on the connected device if "%~1"=="prod" ( echo Installing a production apk - adb install ./app/build/outputs/apk/prod/release/app-prod-universal-release.apk + adb install ./app/build/outputs/apk/release/app-universal-release.apk if ERRORLEVEL 1 ( echo Gradle build failed! Check build_output.txt for details. exit /b 1 ) ) else ( echo Installing a dev apk - adb install ./app/build/outputs/apk/dev/debug/app-dev-universal-debug.apk + adb install ./app/build/outputs/apk/debug/app-universal-debug.apk if ERRORLEVEL 1 ( echo Gradle build failed! Check build_output.txt for details. exit /b 1