diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 00000000..6a0c147f --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,47 @@ +#!/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-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/app/build.gradle.kts b/app/build.gradle.kts index d71aa566..91cd641c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,14 +7,15 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.oss.licenses) } kotlin { compilerOptions { - jvmTarget = JvmTarget.JVM_21 + jvmTarget.set(JvmTarget.JVM_21) } } @@ -28,7 +29,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 2 - versionName = "v1.1.2-rc.1" + versionName = "v1.1.2-rc.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -90,6 +91,8 @@ android { // throw GradleException("Can't Sign Release Build") } + isDebuggable = false + isShrinkResources = true isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") @@ -101,10 +104,13 @@ android { getByName("debug") { applicationIdSuffix = ".dev" versionNameSuffix = "-Dev" + + isDebuggable = true + isShrinkResources = false isMinifyEnabled = false manifestPlaceholders["appIcon"] = "@mipmap/dev_ic_launcher" - manifestPlaceholders["appLabel"] = "Passcodes Dev" + manifestPlaceholders["appLabel"] = "Passcodes-Dev" } create("staging") { @@ -118,35 +124,25 @@ android { applicationIdSuffix = ".staging" versionNameSuffix = "-Staging" - isMinifyEnabled = true - isShrinkResources = true isDebuggable = false + isShrinkResources = true + isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") manifestPlaceholders["appIcon"] = "@mipmap/dev_ic_launcher" - manifestPlaceholders["appLabel"] = "Passcodes Staging" + manifestPlaceholders["appLabel"] = "Passcodes-Staging" } } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - buildFeatures { - viewBinding = true - buildConfig = true - compose = true - } } + compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } + buildFeatures { + viewBinding = true + buildConfig = true compose = true } @@ -194,6 +190,9 @@ dependencies { // Dependency Injection implementation(libs.bundles.koin) + // Datastore Preferences + implementation(libs.bundles.datastore.preferences) + // --- Testing --- diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt index 85af800b..0f3c8787 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt @@ -11,8 +11,8 @@ import android.service.autofill.SaveRequest import android.view.autofill.AutofillValue import android.widget.RemoteViews import com.jeeldobariya.passcodes.R -import com.jeeldobariya.passcodes.data.Passcode -import com.jeeldobariya.passcodes.data.PasscodeDatabase +import com.jeeldobariya.passcodes.autofill.data.Passcode +import com.jeeldobariya.passcodes.autofill.data.PasscodeDatabase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/Passcode.kt similarity index 81% rename from app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt rename to app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/Passcode.kt index e175e96f..61a37394 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/Passcode.kt @@ -1,4 +1,4 @@ -package com.jeeldobariya.passcodes.data +package com.jeeldobariya.passcodes.autofill.data import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDao.kt similarity index 93% rename from app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt rename to app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDao.kt index 957d5a37..9519602d 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDao.kt @@ -1,4 +1,4 @@ -package com.jeeldobariya.passcodes.data +package com.jeeldobariya.passcodes.autofill.data import androidx.room.Dao import androidx.room.Delete diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDatabase.kt similarity index 94% rename from app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt rename to app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDatabase.kt index 1fe03c95..e81ca689 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/data/PasscodeDatabase.kt @@ -1,4 +1,4 @@ -package com.jeeldobariya.passcodes.data +package com.jeeldobariya.passcodes.autofill.data import android.content.Context import androidx.room.Database diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt deleted file mode 100644 index b42f0a89..00000000 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.jeeldobariya.passcodes.flags - -import android.content.Context -import androidx.core.content.edit -import com.jeeldobariya.passcodes.utils.Constant - -class FeatureFlagManager private constructor(context: Context) { - private val prefs = - context.getSharedPreferences(Constant.FEATURE_FLAGS_PREFS_NAME, Context.MODE_PRIVATE) - - var latestFeaturesEnabled: Boolean - get() = prefs.getBoolean(Constant.LATEST_FEATURES_KEY, false) - set(value) { - prefs.edit { putBoolean(Constant.LATEST_FEATURES_KEY, value) } - } - - companion object { - @Volatile - private var INSTANCE: FeatureFlagManager? = null - - fun get(context: Context): FeatureFlagManager { - return INSTANCE ?: synchronized(this) { - val instance = FeatureFlagManager(context.applicationContext) - INSTANCE = instance - instance - } - } - } -} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettings.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettings.kt new file mode 100644 index 00000000..28a6cea4 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettings.kt @@ -0,0 +1,95 @@ +package com.jeeldobariya.passcodes.flags + +// This file contains a solution as commented code... ignore it.. if you don't entertain it. + + +import android.content.Context +// import androidx.datastore.core.DataMigration +import androidx.datastore.dataStore +import kotlinx.serialization.Serializable + +val Context.featureFlagsDatastore by dataStore( + fileName = "feature-flags-settings.json", + serializer = FeatureFlagsSettingsSerializer, + // produceMigrations = { listOf(DataStoreMigration1_2) } +) + +@Serializable +data class FeatureFlagsSettings( + val version: Int = 1, + val isPreviewFeaturesEnabled: Boolean = false, + val isPreviewLayoutEnabled: Boolean = false +) + +/* +Let say we have added a new feature flag, for custom backends.. +like this in Feature Flag Settings. + +```kotlin +@Serializable +data class FeatureFlagsSettings( + val version: Int = 2, + val isPreviewFeaturesEnabled: Boolean = false, + val isPreviewLayoutEnabled: Boolean = false, + val isCustomBackendFeatureEnabled: Boolean = false, +) +``` + +Then we can create a migration like this, and data will be migrated + +```kotlin +data object DataStoreMigration1_2: DataMigration { + override suspend fun cleanUp() { + // I don't think this is need in our case (according to rules written below..) + TODO("Not yet implemented") + } + + override suspend fun migrate(currentData: FeatureFlagsSettings): FeatureFlagsSettings { + return FeatureFlagsSettings( + // this is previous version flags this will be need to restore users current data. + isPreviewFeaturesEnabled = currentData.isPreviewFeaturesEnabled, + isPreviewLayoutEnabled = currentData.isPreviewLayoutEnabled, + + // this is new flag has already have default value, but we will explicitly declare it for clarity reasons.... + isCustomBackendFeatureEnabled = false + + // [!WARNING] + // this should not restore version field from current data.... + // because if we do `version = currentData.version` then it will break the system in next run of migrations. + ) + } + + override suspend fun shouldMigrate(currentData: FeatureFlagsSettings): Boolean { + return currentData.version < 2 + } +} +``` + +## Conclusion: + +I don't know weather this will work or not... let say it will.. +we will eventually find out... + +Here I am just proposing the solution that work perfectly in my mind.. +I have not tested this solution nor do, I have seen this anywhere so I don't know weather this work or not.. +But here are some links that I have refer to, while coming up with the solution... + +- https://medium.com/androiddevelopers/datastore-and-data-migration-fdca806eb1aa +- https://stackoverflow.com/questions/69457920/how-to-perform-version-migrations-in-android-jetpack-datastore +- https://github.com/yogeshpaliyal/KeyPass/blob/master/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt +- https://github.com/PasscodesApp/Passcodes/pull/52 + +they all use preferenceDataStore. which i don;t entertain using... because i guess it too complex.. + +### Rules (Assumptions): + +- To this solution to work we can only add the new field... +- we can't change existing field nor do we can change it's datatype. +- we need to provide the default value to each and every field... +- we can only extend it can;t modify it.... + +**Main Assumption**: the under-lying datastore androidx library, will pass the currentData in `migrate()` function.. +with old field restore and new field will have default value. + +I think for this use case, we won;t need to modify field, so this solution work here.. +*/ diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettingsSerializer.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettingsSerializer.kt new file mode 100644 index 00000000..187978c1 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettingsSerializer.kt @@ -0,0 +1,36 @@ +package com.jeeldobariya.passcodes.flags + +import androidx.datastore.core.Serializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import java.io.InputStream +import java.io.OutputStream + +object FeatureFlagsSettingsSerializer: Serializer { + override val defaultValue: FeatureFlagsSettings + get() = FeatureFlagsSettings() + + override suspend fun readFrom(input: InputStream): FeatureFlagsSettings { + return try { + Json.decodeFromString( + deserializer = FeatureFlagsSettings.serializer(), + string = input.readBytes().decodeToString(), + ) + } catch (e: SerializationException) { + e.printStackTrace() + defaultValue + } + } + + override suspend fun writeTo( + t: FeatureFlagsSettings, + output: OutputStream + ) { + output.write( + Json.encodeToString( + serializer = FeatureFlagsSettings.serializer(), + value = t + ).encodeToByteArray() + ) + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/AboutUsActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/AboutUsActivity.kt index c4ef8a2d..462ec85f 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/AboutUsActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/AboutUsActivity.kt @@ -5,15 +5,19 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri import com.jeeldobariya.passcodes.databinding.ActivityAboutUsBinding -import com.jeeldobariya.passcodes.utils.CommonUtils import com.jeeldobariya.passcodes.utils.Constant +import com.jeeldobariya.passcodes.utils.appDatastore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking class AboutUsActivity : AppCompatActivity() { private lateinit var binding: ActivityAboutUsBinding override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityAboutUsBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LicenseActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LicenseActivity.kt index d31bd781..042c23d4 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LicenseActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LicenseActivity.kt @@ -5,16 +5,20 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.jeeldobariya.passcodes.databinding.ActivityLicenseBinding -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.appDatastore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import java.io.BufferedReader import java.io.InputStreamReader class LicenseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) - var binding = ActivityLicenseBinding.inflate(layoutInflater) + val binding = ActivityLicenseBinding.inflate(layoutInflater) setContentView(binding.root) try { diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LoadPasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LoadPasswordActivity.kt index ef30c244..e2d48a83 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LoadPasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/LoadPasswordActivity.kt @@ -9,8 +9,10 @@ import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.databinding.ActivityLoadPasswordBinding import com.jeeldobariya.passcodes.oldui.adapter.PasswordAdapter -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.appDatastore import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.koin.androidx.viewmodel.ext.android.viewModel class LoadPasswordActivity : AppCompatActivity() { @@ -21,7 +23,9 @@ class LoadPasswordActivity : AppCompatActivity() { private lateinit var passwordAdapter: PasswordAdapter override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityLoadPasswordBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt index 0c6a9fb1..881afa13 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt @@ -7,10 +7,12 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.BuildConfig import com.jeeldobariya.passcodes.databinding.ActivityMainBinding -import com.jeeldobariya.passcodes.utils.CommonUtils import com.jeeldobariya.passcodes.utils.UpdateChecker +import com.jeeldobariya.passcodes.utils.appDatastore import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking // import com.jeeldobariya.passcodes.utils.Permissions @@ -21,7 +23,9 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/PasswordManagerActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/PasswordManagerActivity.kt index f13ee291..0566e216 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/PasswordManagerActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/PasswordManagerActivity.kt @@ -11,11 +11,14 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivityPasswordManagerBinding -import com.jeeldobariya.passcodes.flags.FeatureFlagManager -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.flags.featureFlagsDatastore import com.jeeldobariya.passcodes.utils.Controller +import com.jeeldobariya.passcodes.utils.appDatastore +import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -30,16 +33,13 @@ class PasswordManagerActivity : AppCompatActivity() { private lateinit var importCsvLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityPasswordManagerBinding.inflate(layoutInflater) setContentView(binding.root) - if (!FeatureFlagManager.get(this).latestFeaturesEnabled) { - binding.importPasswordBtn.visibility = GONE - binding.exportPasswordBtn.visibility = GONE - } - controller = Controller(this) // Initialize the controller here importCsvLauncher = registerForActivityResult( @@ -103,6 +103,13 @@ class PasswordManagerActivity : AppCompatActivity() { } } + collectLatestLifecycleFlow(featureFlagsDatastore.data) { + if (!it.isPreviewFeaturesEnabled) { + binding.importPasswordBtn.visibility = GONE + binding.exportPasswordBtn.visibility = GONE + } + } + // Add event onclick listener addOnClickListenerOnButton(binding) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SavePasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SavePasswordActivity.kt index 9833cade..d5a012f7 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SavePasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SavePasswordActivity.kt @@ -5,7 +5,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivitySavePasswordBinding -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.appDatastore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.koin.androidx.viewmodel.ext.android.viewModel class SavePasswordActivity : AppCompatActivity() { @@ -15,7 +17,9 @@ class SavePasswordActivity : AppCompatActivity() { private lateinit var binding: ActivitySavePasswordBinding override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivitySavePasswordBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SettingsActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SettingsActivity.kt index 76adb186..8ba4be4c 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SettingsActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/SettingsActivity.kt @@ -6,17 +6,18 @@ import android.widget.AdapterView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit import androidx.core.os.LocaleListCompat import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivitySettingsBinding -import com.jeeldobariya.passcodes.flags.FeatureFlagManager -import com.jeeldobariya.passcodes.utils.CommonUtils -import com.jeeldobariya.passcodes.utils.Constant +import com.jeeldobariya.passcodes.flags.featureFlagsDatastore import com.jeeldobariya.passcodes.utils.Controller +import com.jeeldobariya.passcodes.utils.appDatastore +import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class SettingsActivity : AppCompatActivity() { @@ -33,14 +34,18 @@ class SettingsActivity : AppCompatActivity() { ) override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) setInitialLangSelection() - binding.switchLatestFeatures.isChecked = FeatureFlagManager.get(this).latestFeaturesEnabled + collectLatestLifecycleFlow(featureFlagsDatastore.data) { + binding.switchLatestFeatures.isChecked = it.isPreviewFeaturesEnabled + } controller = Controller(this) // Initialize the controller here @@ -63,11 +68,6 @@ class SettingsActivity : AppCompatActivity() { } binding.langSwitchDropdown.setSelection(0) - Toast.makeText( - this@SettingsActivity, - getString(R.string.something_went_wrong_msg), - Toast.LENGTH_SHORT - ).show() } // Added all the onclick event listeners @@ -92,17 +92,17 @@ class SettingsActivity : AppCompatActivity() { } binding.toggleThemeBtn.setOnClickListener { - val sharedPrefs = getSharedPreferences(Constant.APP_PREFS_NAME, MODE_PRIVATE) - val currentThemeStyle = - sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) + lifecycleScope.launch { + val currentThemeStyle = appDatastore.data.first().theme + + val currentIndex = THEMES.indexOf(currentThemeStyle) + val nextIndex = (currentIndex + 1) % THEMES.size + val newThemeStyle = THEMES[nextIndex] - val currentIndex = THEMES.indexOf(currentThemeStyle) - val nextIndex = (currentIndex + 1) % THEMES.size - val newThemeStyle = THEMES[nextIndex] + appDatastore.updateData { it.copy(theme = newThemeStyle) } - // Save the new theme and restart the application to apply it - sharedPrefs.edit { putInt(Constant.THEME_KEY, newThemeStyle) } - recreate() + recreate() + } Toast.makeText( this@SettingsActivity, @@ -112,7 +112,11 @@ class SettingsActivity : AppCompatActivity() { } binding.switchLatestFeatures.setOnCheckedChangeListener { _, isChecked -> - FeatureFlagManager.get(this).latestFeaturesEnabled = isChecked + lifecycleScope.launch { + featureFlagsDatastore.updateData { + it.copy(isPreviewFeaturesEnabled = isChecked) + } + } Toast.makeText( this@SettingsActivity, getString(R.string.future_feat_clause) + isChecked.toString(), diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/UpdatePasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/UpdatePasswordActivity.kt index 4824b8f6..901c3f09 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/UpdatePasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/UpdatePasswordActivity.kt @@ -7,8 +7,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivityUpdatePasswordBinding -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.appDatastore import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.koin.androidx.viewmodel.ext.android.viewModel @@ -22,7 +24,9 @@ class UpdatePasswordActivity : AppCompatActivity() { private lateinit var binding: ActivityUpdatePasswordBinding override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityUpdatePasswordBinding.inflate(layoutInflater) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/ViewPasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/ViewPasswordActivity.kt index 6244bcd5..8d1ae41f 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/ViewPasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/ViewPasswordActivity.kt @@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivityViewPasswordBinding -import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.appDatastore import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.koin.androidx.viewmodel.ext.android.viewModel @@ -26,7 +27,9 @@ class ViewPasswordActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) + runBlocking { + setTheme(appDatastore.data.first().theme) + } super.onCreate(savedInstanceState) binding = ActivityViewPasswordBinding.inflate(layoutInflater) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettings.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettings.kt new file mode 100644 index 00000000..ec392d03 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettings.kt @@ -0,0 +1,15 @@ +package com.jeeldobariya.passcodes.utils + +// please refer to `app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagsSettings.kt` for Migration Guide. + +import android.content.Context +import androidx.datastore.dataStore +import com.jeeldobariya.passcodes.R +import kotlinx.serialization.Serializable + +val Context.appDatastore by dataStore(fileName = "app-settings.json", serializer = AppSettingsSerializer) + +@Serializable +data class AppSettings( + val theme: Int = R.style.PasscodesTheme_Default, +) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettingsSerializer.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettingsSerializer.kt new file mode 100644 index 00000000..a8c03f9a --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/AppSettingsSerializer.kt @@ -0,0 +1,36 @@ +package com.jeeldobariya.passcodes.utils + +import androidx.datastore.core.Serializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import java.io.InputStream +import java.io.OutputStream + +object AppSettingsSerializer: Serializer { + override val defaultValue: AppSettings + get() = AppSettings() + + override suspend fun readFrom(input: InputStream): AppSettings { + return try { + Json.decodeFromString( + deserializer = AppSettings.serializer(), + string = input.readBytes().decodeToString(), + ) + } catch (e: SerializationException) { + e.printStackTrace() + defaultValue + } + } + + override suspend fun writeTo( + t: AppSettings, + output: OutputStream + ) { + output.write( + Json.encodeToString( + serializer = AppSettings.serializer(), + value = t + ).encodeToByteArray() + ) + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt deleted file mode 100644 index e5d7f238..00000000 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.jeeldobariya.passcodes.utils - -import android.content.Context -import android.content.Context.MODE_PRIVATE -import com.jeeldobariya.passcodes.R - -class CommonUtils { - companion object { - fun getCurrTheme(context: Context): Int { - val sharedPrefs = context.getSharedPreferences(Constant.APP_PREFS_NAME, MODE_PRIVATE) - val savedThemeStyle = - sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) - return savedThemeStyle - } - - fun updateCurrTheme(context: Context) { - context.setTheme(getCurrTheme(context.applicationContext)) - } - } -} diff --git a/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher.xml index 9c155d17..b287e472 100644 --- a/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher.xml @@ -2,4 +2,5 @@ + diff --git a/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher_round.xml index 9c155d17..b287e472 100644 --- a/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/dev_ic_launcher_round.xml @@ -2,4 +2,5 @@ + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cfe..345888d2 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,6 @@ - - + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52971db0..fd9d9e28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Passcodes - v1.1.2-rc1 + v1.1.2-rc2 Developed by: Dobariya Jeel diff --git a/build.gradle.kts b/build.gradle.kts index 81899815..0dc3239c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,13 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.jetbrains.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.oss.licenses) apply false } -// Allprojects block is common for setting up common repositories for all subprojects. +// All projects block is common for setting up common repositories for all subprojects. allprojects { repositories { google() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f964c6e2..4f06ce93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.2.21" material = "1.13.0" -okhttp = "5.3.0" +okhttp = "5.3.2" oss-license = "17.3.0" appcompat = "1.7.1" room = "2.8.3" @@ -19,13 +19,16 @@ compose-viewmodel = "2.9.4" nav3Core = "1.0.0-rc01" lifecycleViewmodelNav3 = "2.10.0-rc01" material3AdaptiveNav3 = "1.3.0-alpha03" +datastorePreferences = "1.1.7" +kotlinSerializationJson = "1.9.0" # Plugin versions agp = "8.13.1" -kotlin-plugin = "2.2.21" ksp = "2.3.2" oss-license-plugin = "0.10.9" # Also update in settings.gradle.kts + + [libraries] # Kotlin Std libs kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -49,6 +52,10 @@ material = { group = "com.google.android.material", name = "material", version.r oss-license = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "oss-license" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +# DataStore Preference +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerializationJson" } + # Room room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } @@ -70,6 +77,7 @@ lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmode koin = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +# Json json = { group = "org.json", name = "json", version.ref = "json" } # Testing @@ -83,33 +91,39 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } + [bundles] -# Grouping Jetpack Compose Dependencies +# Jetpack Compose Dependencies compose = ["compose-ui-material", "compose-ui-preview", "compose-activity", "compose-viewmodel"] compose-debug = ["compose-ui-tooling-debug"] -# Grouping Navigation 3 Dependencies +# Navigation 3 Dependencies navigation3 = ["androidx-navigation3-runtime", "androidx-navigation3-ui", "androidx-lifecycle-viewmodel-navigation3", "androidx-material3-adaptive-navigation3"] -# Grouping Android Architecture Releated Components +# Android Architecture Releated Components room = ["room-ktx", "room-testing"] lifecycle = ["lifecycle-runtime", "lifecycle-viewmodel"] -# Grouping Coroutines Dependencies +# DataStore Preference +datastore-preferences = ["androidx-datastore-preferences", "kotlinx-serialization-json"] + +# Coroutines Dependencies coroutines = ["coroutines-core", "coroutines-android"] coroutines-test = ["coroutines-core", "coroutines-test"] -# Grouping Dependency Injection Dependencies +# Dependency Injection Dependencies koin = ["koin", "koin-compose"] -# Grouping General Test Dependencies +# General Test Dependencies unit-test = ["junit", "truth"] android-test = ["androidx-test-ext-junit", "espresso-core"] + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-plugin" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } oss-licenses = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "oss-license-plugin" }