diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index ea24eba2..92ea8d8b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -55,6 +55,8 @@ jobs: run: | echo "Release Version: $RELEASE_VERSION" + - name: Stop gradle daemons + run: ./gradlew --stop - name: Build project run: ./gradlew assembleDebug - name: Checks diff --git a/.gitignore b/.gitignore index adf08f83..bf21d827 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ app/.classpath app/.project app/release -auth/ #core/ .idea/ .project diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 67cc0377..391fa2be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,7 +4,9 @@ import java.util.Properties import kotlin.concurrent.thread plugins { - id("pvnclient.android.application") + id("org.fptn.vpn.application") + id("org.fptn.vpn.application.compose") + id("org.fptn.vpn.application.koin") id("com.google.gms.google-services") alias(libs.plugins.crashlytics) } @@ -15,7 +17,9 @@ android { namespace = "org.fptn.vpn" compileSdk = rootProject.extra.get("compileSdkVersion") as Int ndkVersion = "28.1.13356709" + var isCI = System.getenv("KEY_ALIAS") != null + signingConfigs { create("release") { if (isCI) { @@ -113,12 +117,23 @@ android { dependencies { implementation(platform(libs.firebase.bom)) + implementation(project(":auth:domain")) + implementation(project(":auth:ui")) implementation(project(":core:common")) + implementation(project(":core:designsystem")) + implementation(project(":core:model")) + implementation(project(":core:network")) + implementation(project(":core:persistent")) + implementation(project(":home:ui")) + implementation(project(":settings:ui")) implementation(project(":vpnclient")) implementation(libs.androidx.activity) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) - // To use CallbackToFutureAdapter + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.ui) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.monitor) implementation(libs.androidx.room.guava) implementation(libs.androidx.room.runtime) @@ -127,6 +142,7 @@ dependencies { implementation(libs.guava) implementation(libs.ipaddress) implementation(libs.jackson.databind) + implementation(libs.koin.android) implementation(libs.material) implementation(libs.zxing) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2cda1143..d2d1fcac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ tools:node="remove" /> + android:label="@string/app_name" + android:screenOrientation="portrait" + android:theme="@style/Theme.FptnClient.Splash" + android:windowSoftInputMode="adjustResize"> + android:noHistory="true" + android:screenOrientation="portrait" /> + android:screenOrientation="portrait" /> + android:screenOrientation="portrait" /> + android:screenOrientation="portrait" /> { + } + is AuthActivityUiState.Login -> { + AuthScreen() + } + is AuthActivityUiState.Main -> { + val context = LocalContext.current + context.startActivity(Intent(context, HomeActivity::class.java)) + } + } + } + } + } +} + +@Suppress("MagicNumber") +private val lightScrim = Color.argb(0xe6, 0xFF, 0xFF, 0xFF) + +@Suppress("MagicNumber") +private val darkScrim = Color.argb(0x80, 0x1b, 0x1b, 0x1b) diff --git a/app/src/main/kotlin/org/fptn/vpn/viewmodel/MainViewModel.kt b/app/src/main/kotlin/org/fptn/vpn/viewmodel/MainViewModel.kt new file mode 100644 index 00000000..2934d434 --- /dev/null +++ b/app/src/main/kotlin/org/fptn/vpn/viewmodel/MainViewModel.kt @@ -0,0 +1,50 @@ +package org.fptn.vpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.fptn.vpn.auth.domain.AuthInteractor +import org.fptn.vpn.core.common.Result +import org.fptn.vpn.core.common.asResult +import org.fptn.vpn.core.model.FptnUserDomain + +class MainViewModel( + authInteractor: AuthInteractor, +) : ViewModel() { + val uiState: StateFlow = + authInteractor.user + .asResult() + .map { result -> + when (result) { + is Result.Error -> AuthActivityUiState.Login + is Result.Loading -> AuthActivityUiState.Loading + is Result.Success -> AuthActivityUiState.Main(result.data) + } + }.stateIn( + scope = viewModelScope, + initialValue = AuthActivityUiState.Loading, + started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS), + ) + + companion object { + private const val STOP_TIMEOUT_MILLIS = 5_000L + } +} + +sealed interface AuthActivityUiState { + data object Loading : AuthActivityUiState + + data object Login : AuthActivityUiState + + data class Main( + val userData: FptnUserDomain, + ) : AuthActivityUiState + + /** + * Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen. + */ + fun shouldKeepSplashScreen() = this is Loading +} diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml new file mode 100644 index 00000000..144393be --- /dev/null +++ b/app/src/main/res/drawable/ic_splash.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..928ff71d --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + #FCFCFC + #000000 + \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index ab92c849..53d01b54 100644 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -17,4 +17,8 @@ #1B023B #FF03DAC5 + + #000000 + #000000 + #FCFCFC \ 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 d84ceb4f..ae1c8be8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,4 +125,8 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn Invalid token format! Check the correctness of the input. + Home + Settings + ⚠️ You aren’t connected to the internet + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 6e890323..9ced1d09 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,4 @@ - + + + + + + diff --git a/auth/data/.gitignore b/auth/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/auth/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/auth/data/build.gradle.kts b/auth/data/build.gradle.kts new file mode 100644 index 00000000..df50c16c --- /dev/null +++ b/auth/data/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("org.fptn.vpn.library.android") + id("org.fptn.vpn.library.koin") +} + +android { + namespace = "org.fptn.vpn.auth.data" +} + +dependencies { + implementation(project(":auth:domain")) + implementation(project(":core:common")) + implementation(project(":core:model")) + implementation(project(":core:persistent")) + implementation(libs.koin.annotations.jvm) + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + + testImplementation(libs.kotlin.test) + testImplementation(libs.mockk) +} diff --git a/auth/data/proguard-rules.pro b/auth/data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/auth/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/AuthRepositoryImpl.kt b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/AuthRepositoryImpl.kt new file mode 100644 index 00000000..1660be3a --- /dev/null +++ b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/AuthRepositoryImpl.kt @@ -0,0 +1,42 @@ +package org.fptn.vpn.auth.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import org.fptn.vpn.auth.domain.AuthRepository +import org.fptn.vpn.auth.domain.token.AuthTokenDecoder +import org.fptn.vpn.core.common.AppDispatchers.DISPATCHER_IO +import org.fptn.vpn.core.model.FptnUserDomain +import org.fptn.vpn.core.persistent.PreferenceStore +import org.fptn.vpn.core.persistent.model.FptnServerDao +import org.fptn.vpn.core.persistent.model.FptnServerDbModel +import org.fptn.vpn.core.persistent.model.toDbModel +import org.koin.core.annotation.Named + +class AuthRepositoryImpl( + private val serverDao: FptnServerDao, + private val preferenceStore: PreferenceStore, + private val tokenDecoder: AuthTokenDecoder, + @Named(DISPATCHER_IO) private val dispatcher: CoroutineDispatcher, +) : AuthRepository { + override val user: Flow = preferenceStore.token.map { tokenDecoder.decode(it) } + + override suspend fun saveToken(token: String) { + withContext(dispatcher) { + preferenceStore.updateToken(token) + val model: FptnUserDomain = tokenDecoder.decode(token) + val username = model.username + val password = model.password + val servers: List = model.servers.map { it.toDbModel(username, password, false) } + val censoredZoneServers: List = + model.censoredZoneServers.map { it.toDbModel(username, password, true) } + val allServers = servers + censoredZoneServers + allServers.map { async { serverDao.insert(it) } }.awaitAll() + } + } + + override suspend fun logout() = preferenceStore.clearAllData() +} diff --git a/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/di/AuthDataModule.kt b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/di/AuthDataModule.kt new file mode 100644 index 00000000..afcf509d --- /dev/null +++ b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/di/AuthDataModule.kt @@ -0,0 +1,29 @@ +package org.fptn.vpn.auth.data.di + +import org.fptn.vpn.auth.data.AuthRepositoryImpl +import org.fptn.vpn.auth.data.token.AuthTokenDecoderImpl +import org.fptn.vpn.auth.data.token.AuthTokenNormalizerImpl +import org.fptn.vpn.auth.domain.AuthRepository +import org.fptn.vpn.auth.domain.token.AuthTokenDecoder +import org.fptn.vpn.auth.domain.token.AuthTokenNormalizer +import org.fptn.vpn.core.common.AppDispatchers.DISPATCHER_IO +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val authDataModule = + module { + single { + AuthRepositoryImpl( + get(), + get(), + get(), + get(named(DISPATCHER_IO)), + ) + } + } + +val authTokenModule = + module { + single { AuthTokenNormalizerImpl() } + single { AuthTokenDecoderImpl(get()) } + } diff --git a/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderImpl.kt b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderImpl.kt new file mode 100644 index 00000000..4d7e2d4d --- /dev/null +++ b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderImpl.kt @@ -0,0 +1,19 @@ +package org.fptn.vpn.auth.data.token + +import kotlinx.serialization.json.Json +import org.fptn.vpn.auth.domain.token.AuthTokenDecoder +import org.fptn.vpn.auth.domain.token.AuthTokenNormalizer +import org.fptn.vpn.core.model.FptnUser +import org.fptn.vpn.core.model.FptnUserDomain +import org.fptn.vpn.core.model.toDomain +import kotlin.io.encoding.Base64 + +class AuthTokenDecoderImpl( + val tokenNormalizer: AuthTokenNormalizer, +) : AuthTokenDecoder { + override fun decode(token: String): FptnUserDomain { + val normalizedToken = tokenNormalizer.normalize(token) + val decodedBytes = String(Base64.decode(normalizedToken), Charsets.UTF_8) + return Json.decodeFromString(decodedBytes).toDomain() + } +} diff --git a/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerImpl.kt b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerImpl.kt new file mode 100644 index 00000000..51b810eb --- /dev/null +++ b/auth/data/src/main/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerImpl.kt @@ -0,0 +1,21 @@ +package org.fptn.vpn.auth.data.token + +import org.fptn.vpn.auth.domain.token.AuthTokenNormalizer + +class AuthTokenNormalizerImpl : AuthTokenNormalizer { + override fun normalize(token: String): String { + val normalizedToken = + token + .replace("\\s+".toRegex(), "") + .replace("fptn://", "") + .replace("fptn:", "") + val padding = (OFFSET - normalizedToken.length % OFFSET) % OFFSET + val result = StringBuilder(normalizedToken) + repeat(padding) { result.append("=") } + return result.toString() + } + + private companion object { + const val OFFSET = 4 + } +} diff --git a/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderTest.kt b/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderTest.kt new file mode 100644 index 00000000..035fb1fa --- /dev/null +++ b/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenDecoderTest.kt @@ -0,0 +1,233 @@ +package org.fptn.vpn.auth.data.token + +import io.mockk.every +import io.mockk.mockk +import org.fptn.vpn.auth.domain.token.AuthTokenDecoder +import org.fptn.vpn.auth.domain.token.AuthTokenNormalizer +import org.fptn.vpn.core.model.FptnServerDomain +import org.fptn.vpn.core.model.FptnUserDomain +import org.junit.Assert.assertThrows +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AuthTokenDecoderTest { + private val normalizer = mockk() + private val decoder: AuthTokenDecoder by lazy { AuthTokenDecoderImpl(normalizer) } + + @Test + fun `decode successful`() { + val token = + "eyJ2ZXJzaW9uIjogMSwgInNlcnZpY2VfbmFtZSI6ICJGUFROLk9OTElORSIsICJ1c2VybmFtZSI6ICJteV9hd2Vz" + + "b21lX3VzZXJfbmFtZSIsICJwYXNzd29yZCI6ICJteV9hd2Vzb21lX3Bhc3N3b3JkIiwgInNlcnZlcnMiOiBbeyJ" + + "uYW1lIjogIkVzdG9uaWEiLCAiaG9zdCI6ICIxOTIuMTY4LjEuMiIsICJtZDVfZmluZ2VycHJpbnQiOiAiZDAwOWZ" + + "kOWNlYjI4MzEyMzgzMmU1YWQzZWNhMDIzNGEiLCAicG9ydCI6IDQ0M30sIHsibmFtZSI6ICJMYXR2aWEtMSIsICJ" + + "ob3N0IjogIjE5Mi4xNjguMS4zIiwgIm1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzODMyZTVhZDN" + + "lY2EwMjM0YiIsICJwb3J0IjogNDQzfSwgeyJuYW1lIjogIkxhdHZpYS0yIiwgImhvc3QiOiAiMTkyLjE2OC4xLjQiL" + + "CAibWQ1X2ZpbmdlcnByaW50IjogImQwMDlmZDljZWIyODMxMjM4MzJlNWFkM2VjYTAyMzRj" + + "IiwgInBvcnQiOiA0NDN9LCB7Im5hbWUiOiAiTmV0aGVybGFuZHMtMSIsICJob3N0IjogIjE5Mi4xNjguMS41IiwgIm" + + "1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzODMyZTVhZDNlY2EwMjM0ZCIsICJwb3J0IjogNDQzfS" + + "wgeyJuYW1lIjogIlVTQS1TZWF0dGxlIiwgImhvc3QiOiAiMTkyLjE2OC4xLjYiLCAibWQ1X2ZpbmdlcnByaW50Ij" + + "ogImQwMDlmZDljZWIyODMxMjM4MzJlNWFkM2VjYTAyMzJhIiwgInBvcnQiOiA0NDN9LCB7Im5hbWUiOiAiSmFwYW" + + "4tMSIsICJob3N0IjogIjE5Mi4xNjguMS43IiwgIm1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzODMy" + + "ZTVhZDNlY2EwMjMyYiIsICJwb3J0IjogNDQzfV0sICJjZW5zb3JlZF96b25lX3NlcnZlcnMiOiBbeyJuYW1lIjogI" + + "lJ1c3NpYSAoU2FpbnQgUGV0ZXJzYnVyZykiLCAiaG9zdCI6ICIxOTIuMTY4LjEuOCIsICJtZDVfZmluZ2VycHJpbn" + + "QiOiAiZDAwOWZkOWNlYjI4MzEyMzgzMmU1YWQzZWNhMDIzMmMiLCAicG9ydCI6IDQ0M31dfQ==" + every { normalizer.normalize(token) } returns token + + val res = decoder.decode(token) + + val expected = + FptnUserDomain( + version = 1, + serviceName = "FPTN.ONLINE", + username = "my_awesome_user_name", + password = "my_awesome_password", + servers = + listOf( + FptnServerDomain( + name = "Estonia", + host = "192.168.1.2", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234a", + port = 443, + ), + FptnServerDomain( + name = "Latvia-1", + host = "192.168.1.3", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234b", + port = 443, + ), + FptnServerDomain( + name = "Latvia-2", + host = "192.168.1.4", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234c", + port = 443, + ), + FptnServerDomain( + name = "Netherlands-1", + host = "192.168.1.5", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234d", + port = 443, + ), + FptnServerDomain( + name = "USA-Seattle", + host = "192.168.1.6", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232a", + port = 443, + ), + FptnServerDomain( + name = "Japan-1", + host = "192.168.1.7", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232b", + port = 443, + ), + ), + censoredZoneServers = + listOf( + FptnServerDomain( + name = "Russia (Saint Petersburg)", + host = "192.168.1.8", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232c", + port = 443, + ), + ), + ) + assertEquals(expected, res) + } + + @Test + fun `decode with both empty servers`() { + val token = + "eyJ2ZXJzaW9uIjogMSwgInNlcnZpY2VfbmFtZSI6ICJGUFROLk9OTElORSIsICJ1c2VybmFtZSI6ICJteV9hd" + + "2Vzb21lX3VzZXJfbmFtZSIsICJwYXNzd29yZCI6ICJteV9hd2Vzb21lX3Bhc3N3b3JkIiwgInNlcnZlcnMiO" + + "iBbXSwgImNlbnNvcmVkX3pvbmVfc2VydmVycyI6IFtdfQ==" + every { normalizer.normalize(token) } returns token + + val res = decoder.decode(token) + + val expected = + FptnUserDomain( + version = 1, + serviceName = "FPTN.ONLINE", + username = "my_awesome_user_name", + password = "my_awesome_password", + ) + assertEquals(expected, res) + } + + @Test + fun `decode with empty servers`() { + val token = + "eyJ2ZXJzaW9uIjogMSwgInNlcnZpY2VfbmFtZSI6ICJGUFROLk9OTElORSIsICJ1c2VybmFtZSI6ICJteV9hd2Vzb" + + "21lX3VzZXJfbmFtZSIsICJwYXNzd29yZCI6ICJteV9hd2Vzb21lX3Bhc3N3b3JkIiwgInNlcnZlcnMiOiBb" + + "XSwgImNlbnNvcmVkX3pvbmVfc2VydmVycyI6IFt7Im5hbWUiOiAiUnVzc2lhIChTYWludCBQZXRlcnNidXJnKS" + + "IsICJob3N0IjogIjE5Mi4xNjguMS44IiwgIm1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzOD" + + "MyZTVhZDNlY2EwMjMyYyIsICJwb3J0IjogNDQzfV19" + every { normalizer.normalize(token) } returns token + + val res = decoder.decode(token) + + val expected = + FptnUserDomain( + version = 1, + serviceName = "FPTN.ONLINE", + username = "my_awesome_user_name", + password = "my_awesome_password", + censoredZoneServers = + listOf( + FptnServerDomain( + name = "Russia (Saint Petersburg)", + host = "192.168.1.8", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232c", + port = 443, + ), + ), + ) + assertEquals(expected, res) + } + + @Test + fun `decode with empty censored servers`() { + val token = + "eyJ2ZXJzaW9uIjogMSwgInNlcnZpY2VfbmFtZSI6ICJGUFROLk9OTElORSIsICJ1c2VybmFtZSI6ICJteV9hd2Vzb2" + + "1lX3VzZXJfbmFtZSIsICJwYXNzd29yZCI6ICJteV9hd2Vzb21lX3Bhc3N3b3JkIiwgInNlcnZlcnMiOiBbeyJuYW" + + "1lIjogIkVzdG9uaWEiLCAiaG9zdCI6ICIxOTIuMTY4LjEuMiIsICJtZDVfZmluZ2VycHJpbnQiOiAiZDAwOWZkOWN" + + "lYjI4MzEyMzgzMmU1YWQzZWNhMDIzNGEiLCAicG9ydCI6IDQ0M30sIHsibmFtZSI6ICJMYXR2aWEtMSIsICJob3N0I" + + "jogIjE5Mi4xNjguMS4zIiwgIm1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzODMyZTVhZDNlY2EwMjM" + + "0YiIsICJwb3J0IjogNDQzfSwgeyJuYW1lIjogIkxhdHZpYS0yIiwgImhvc3QiOiAiMTkyLjE2OC4xLjQiLCAibWQ1X2" + + "ZpbmdlcnByaW50IjogImQwMDlmZDljZWIyODMxMjM4MzJlNWFkM2VjYTAyMzRjIiwgInBvcnQiOiA0NDN9LCB7Im5h" + + "bWUiOiAiTmV0aGVybGFuZHMtMSIsICJob3N0IjogIjE5Mi4xNjguMS41IiwgIm1kNV9maW5nZXJwcmludCI6ICJkMD" + + "A5ZmQ5Y2ViMjgzMTIzODMyZTVhZDNlY2EwMjM0ZCIsICJwb3J0IjogNDQzfSwgeyJuYW1lIjogIlVTQS1TZWF0dGxl" + + "IiwgImhvc3QiOiAiMTkyLjE2OC4xLjYiLCAibWQ1X2ZpbmdlcnByaW50IjogImQwMDlmZDljZWIyODMxMjM4MzJlNW" + + "FkM2VjYTAyMzJhIiwgInBvcnQiOiA0NDN9LCB7Im5hbWUiOiAiSmFwYW4tMSIsICJob3N0IjogIjE5Mi4xNjguMS43" + + "IiwgIm1kNV9maW5nZXJwcmludCI6ICJkMDA5ZmQ5Y2ViMjgzMTIzODMyZTVhZDNlY2EwMjMyYiIsICJwb3J0IjogN" + + "DQzfV0sICJjZW5zb3JlZF96b25lX3NlcnZlcnMiOiBbXX0=" + every { normalizer.normalize(token) } returns token + + val res = decoder.decode(token) + + val expected = + FptnUserDomain( + version = 1, + serviceName = "FPTN.ONLINE", + username = "my_awesome_user_name", + password = "my_awesome_password", + servers = + listOf( + FptnServerDomain( + name = "Estonia", + host = "192.168.1.2", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234a", + port = 443, + ), + FptnServerDomain( + name = "Latvia-1", + host = "192.168.1.3", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234b", + port = 443, + ), + FptnServerDomain( + name = "Latvia-2", + host = "192.168.1.4", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234c", + port = 443, + ), + FptnServerDomain( + name = "Netherlands-1", + host = "192.168.1.5", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0234d", + port = 443, + ), + FptnServerDomain( + name = "USA-Seattle", + host = "192.168.1.6", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232a", + port = 443, + ), + FptnServerDomain( + name = "Japan-1", + host = "192.168.1.7", + md5Fingerprint = "d009fd9ceb283123832e5ad3eca0232b", + port = 443, + ), + ), + ) + assertEquals(expected, res) + } + + @Test + fun `decode with exception empty servers`() { + val invalidToken = + "eyJ2ZXJzaW9uIjogMSwgInNlcnZpY2VfbmFtZSI6ICJGUFROLk9OTElORSIsICJ1c2VybmFtZSI6ICJteV9hd2Vzb21lX" + + "3VzZXJfbmFtZSIsICJwYXNzd29yZCI6ICJteV9hd2Vzb21lX3Bhc3N3b3JkIiwgInNlcnZlcnMiOiBbXSwgImNlbn" + + "NvcmVkX3pvbmVfc2VydmVycyI6IFtdfQ" + every { normalizer.normalize(invalidToken) } returns invalidToken + + val exception = + assertThrows(IllegalArgumentException::class.java) { + decoder.decode(invalidToken) + } + + assertTrue(exception.message?.isNotEmpty() == true) + } +} diff --git a/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerTest.kt b/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerTest.kt new file mode 100644 index 00000000..7b67bbcb --- /dev/null +++ b/auth/data/src/test/kotlin/org/fptn/vpn/auth/data/token/AuthTokenNormalizerTest.kt @@ -0,0 +1,65 @@ +package org.fptn.vpn.auth.data.token + +import org.fptn.vpn.auth.domain.token.AuthTokenNormalizer +import org.junit.Assert.assertEquals +import org.junit.Test + +class AuthTokenNormalizerTest { + private val normalizer: AuthTokenNormalizer = AuthTokenNormalizerImpl() + + @Test + fun `normalize should remove whitespaces`() { + val input = " token with spacess " + val expected = "tokenwithspacess" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `normalize should remove fptn protocol prefix`() { + val input = "fptn://mysome-token" + val expected = "mysome-token" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `normalize should remove fptn prefix`() { + val input = "fptn:mysome-token" + val expected = "mysome-token" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `normalize should handle combined cases`() { + val input = " fptn:// token with spacess " + val expected = "tokenwithspacess" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `normalize should return empty string for empty input`() { + val input = "" + val expected = "" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `token has no padding`() { + val input = "token123" + val expected = "token123" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `token has one padding`() { + val input = "token12" + val expected = "token12=" + assertEquals(expected, normalizer.normalize(input)) + } + + @Test + fun `token has two paddings`() { + val input = "token1234" + val expected = "token1234===" + assertEquals(expected, normalizer.normalize(input)) + } +} diff --git a/auth/domain/.gitignore b/auth/domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/auth/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/auth/domain/build.gradle.kts b/auth/domain/build.gradle.kts new file mode 100644 index 00000000..85f01cdc --- /dev/null +++ b/auth/domain/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("org.fptn.vpn.library.kotlin") + id("org.fptn.vpn.library.koin") +} + +dependencies { + implementation(project(":core:model")) + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) +} diff --git a/auth/domain/proguard-rules.pro b/auth/domain/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/auth/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthInteractor.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthInteractor.kt new file mode 100644 index 00000000..77f9d1ff --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthInteractor.kt @@ -0,0 +1,22 @@ +package org.fptn.vpn.auth.domain + +import kotlinx.coroutines.flow.Flow +import org.fptn.vpn.core.model.FptnUserDomain + +interface AuthInteractor { + val user: Flow + + suspend fun saveToken(token: String) + + suspend fun logout() +} + +class AuthInteractorImpl( + private val authRepository: AuthRepository, +) : AuthInteractor { + override val user: Flow = authRepository.user + + override suspend fun saveToken(token: String) = authRepository.saveToken(token) + + override suspend fun logout() = authRepository.logout() +} diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthRepository.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthRepository.kt new file mode 100644 index 00000000..4aa3bd04 --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/AuthRepository.kt @@ -0,0 +1,12 @@ +package org.fptn.vpn.auth.domain + +import kotlinx.coroutines.flow.Flow +import org.fptn.vpn.core.model.FptnUserDomain + +interface AuthRepository { + val user: Flow + + suspend fun saveToken(token: String) + + suspend fun logout() +} diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/di/AuthDomainModule.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/di/AuthDomainModule.kt new file mode 100644 index 00000000..59bb22bd --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/di/AuthDomainModule.kt @@ -0,0 +1,10 @@ +package org.fptn.vpn.auth.domain.di + +import org.fptn.vpn.auth.domain.AuthInteractor +import org.fptn.vpn.auth.domain.AuthInteractorImpl +import org.koin.dsl.module + +val authDomainModule = + module { + single { AuthInteractorImpl(get()) } + } diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/exception/InvalidTokenException.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/exception/InvalidTokenException.kt new file mode 100644 index 00000000..8b105e92 --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/exception/InvalidTokenException.kt @@ -0,0 +1,5 @@ +package org.fptn.vpn.auth.domain.exception + +class InvalidTokenException( + message: String, +) : Exception(message) diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenDecoder.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenDecoder.kt new file mode 100644 index 00000000..929511ad --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenDecoder.kt @@ -0,0 +1,7 @@ +package org.fptn.vpn.auth.domain.token + +import org.fptn.vpn.core.model.FptnUserDomain + +interface AuthTokenDecoder { + fun decode(token: String): FptnUserDomain +} diff --git a/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenNormalizer.kt b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenNormalizer.kt new file mode 100644 index 00000000..d60b67fc --- /dev/null +++ b/auth/domain/src/main/kotlin/org/fptn/vpn/auth/domain/token/AuthTokenNormalizer.kt @@ -0,0 +1,5 @@ +package org.fptn.vpn.auth.domain.token + +interface AuthTokenNormalizer { + fun normalize(token: String): String +} diff --git a/auth/ui/.gitignore b/auth/ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/auth/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/auth/ui/build.gradle.kts b/auth/ui/build.gradle.kts new file mode 100644 index 00000000..982102b3 --- /dev/null +++ b/auth/ui/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("org.fptn.vpn.library.android") + id("org.fptn.vpn.library.koin") + id("org.fptn.vpn.library.android.compose") +} + +android { + namespace = "org.fptn.vpn.auth.ui" +} + +dependencies { + + implementation(project(":auth:data")) + implementation(project(":auth:domain")) + implementation(project(":core:common")) + implementation(project(":core:designsystem")) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.core) + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/auth/ui/proguard-rules.pro b/auth/ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/auth/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthModule.kt b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthModule.kt new file mode 100644 index 00000000..a8d9693c --- /dev/null +++ b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthModule.kt @@ -0,0 +1,22 @@ +package com.filantrop.pvnclient.auth.ui + +import org.fptn.vpn.auth.data.di.authDataModule +import org.fptn.vpn.auth.data.di.authTokenModule +import org.fptn.vpn.auth.domain.di.authDomainModule +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val viewModelModule = + module { + viewModel { AuthViewModel(get()) } + } + +val authModule = + module { + includes( + authTokenModule, + viewModelModule, + authDataModule, + authDomainModule, + ) + } diff --git a/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthScreen.kt b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthScreen.kt new file mode 100644 index 00000000..7ea1e618 --- /dev/null +++ b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthScreen.kt @@ -0,0 +1,166 @@ +package com.filantrop.pvnclient.auth.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.fptn.vpn.auth.ui.R +import org.fptn.vpn.core.common.Constants.SPACE +import org.fptn.vpn.core.designsystem.icons.PvnIcons +import org.koin.androidx.compose.koinViewModel + +private const val WEIGHT = 0.5f + +@Composable +fun AuthScreen(viewModel: AuthViewModel = koinViewModel()) { + val uiState: AuthState by viewModel.uiState.collectAsStateWithLifecycle() + AuthScreen( + uiState, + onTokenChanged = viewModel::changeToken, + onLoginClick = viewModel::login, + ) +} + +@Composable +@Suppress("UnusedParameter") +fun AuthScreen( + state: AuthState, + onTokenChanged: (token: String) -> Unit, + onLoginClick: () -> Unit, +) { + Scaffold( + modifier = Modifier.navigationBarsPadding(), + ) { padding: PaddingValues -> + Column( + modifier = + Modifier + .padding(padding) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Spacer(modifier = Modifier.weight(WEIGHT)) + Card( + shape = CircleShape, + border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary), + modifier = Modifier.padding(16.dp), + elevation = CardDefaults.cardElevation(4.dp), + ) { + Icon( + modifier = + Modifier + .size(148.dp), + imageVector = PvnIcons.Person, + contentDescription = "person", + tint = MaterialTheme.colorScheme.primary, + ) + } + ClickableLink() + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 16.dp), + ) { + OutlinedTextField( + value = state.token, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + onValueChange = onTokenChanged, + label = { Text(stringResource(R.string.enter_token_hint)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) + } + Button( + onClick = onLoginClick, + modifier = Modifier.padding(horizontal = 32.dp), + ) { Text(stringResource(R.string.login)) } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Composable +fun ClickableLink() { + val annotatedString = + buildAnnotatedString { + withStyle( + style = + SpanStyle( + color = MaterialTheme.colorScheme.primary, + ), + ) { + append(stringResource(id = R.string.use_telegram_bot)) + append(SPACE) + } + withLink(LinkAnnotation.Url(url = stringResource(id = R.string.telegram_bot_link))) { + withStyle( + style = + SpanStyle( + color = MaterialTheme.colorScheme.secondary, + textDecoration = TextDecoration.Underline, + ), + ) { + append(stringResource(id = R.string.telegram_bot)) + } + } + withStyle( + style = + SpanStyle( + color = MaterialTheme.colorScheme.primary, + ), + ) { + append(SPACE) + append(stringResource(id = R.string.get_token_telegram_bot)) + } + } + Text( + text = annotatedString, + modifier = + Modifier + .padding(dimensionResource(id = org.fptn.vpn.core.designsystem.R.dimen.padding_medium)), + ) +} + +@Preview(showBackground = true) +@Composable +fun DefaultAuthScreen() { + AuthScreen( + AuthState(token = "123"), + {}, + {}, + ) +} diff --git a/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthViewModel.kt b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthViewModel.kt new file mode 100644 index 00000000..6cc62599 --- /dev/null +++ b/auth/ui/src/main/kotlin/com/filantrop/pvnclient/auth/ui/AuthViewModel.kt @@ -0,0 +1,30 @@ +package com.filantrop.pvnclient.auth.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.fptn.vpn.auth.domain.AuthInteractor + +data class AuthState( + val token: String, +) + +class AuthViewModel( + private val authInteractor: AuthInteractor, +) : ViewModel() { + private val _uiState = MutableStateFlow(AuthState("")) + val uiState: StateFlow = _uiState.asStateFlow() + + fun changeToken(token: String) { + _uiState.update { it.copy(token = token) } + } + + fun login() = + viewModelScope.launch { + authInteractor.saveToken(_uiState.value.token) + } +} diff --git a/auth/ui/src/main/res/values-ru/strings.xml b/auth/ui/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..fd2f0c3e --- /dev/null +++ b/auth/ui/src/main/res/values-ru/strings.xml @@ -0,0 +1,8 @@ + + + Используйте + Telegram-bot + для получения токена + Войти + Вставьте ваш токен + \ No newline at end of file diff --git a/auth/ui/src/main/res/values/strings.xml b/auth/ui/src/main/res/values/strings.xml new file mode 100644 index 00000000..9f63a3b2 --- /dev/null +++ b/auth/ui/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + https://t.me/fptn_bot + Use + the Telegram-bot + to get a token + Login + Paste your token here + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 01c364f4..760e5349 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,9 +75,11 @@ tasks.register("clean", Delete::class) { } plugins { + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.crashlytics) apply false alias(libs.plugins.deps.sorting) apply false alias(libs.plugins.deps.unused) apply true + alias(libs.plugins.serialization) apply false } applyPrecheckOptions() diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a68391c5..9e1cf534 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,25 +8,46 @@ buildscript { } } -gradlePlugin { - plugins { - register("androidApplication") { - id = "pvnclient.android.application" - implementationClass = "AndroidApplicationConventionPlugin" - } - register("kotlinLibrary") { - id = "pvnclient.android.library.kotlin" - implementationClass = "KotlinLibraryConventionPlugin" - } - } -} - dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) - implementation(libs.detekt) implementation(libs.android.gradle.plugin) + implementation(libs.detekt) + implementation(libs.java.poet) implementation(libs.guava) implementation(libs.kotlin.gradle.plugin) implementation(libs.kotlin.metadata.jvm) implementation(libs.ksp.gradle.plugin) } + +gradlePlugin { + plugins { + register("androidApplication") { + id = "org.fptn.vpn.application" + implementationClass = "org.fptn.vpn.gradle.AndroidApplicationConventionPlugin" + } + register("androidApplicationCompose") { + id = "org.fptn.vpn.application.compose" + implementationClass = "org.fptn.vpn.gradle.AndroidApplicationComposeConventionPlugin" + } + register("androidApplicationKoin") { + id = "org.fptn.vpn.application.koin" + implementationClass = "org.fptn.vpn.gradle.AndroidApplicationKoinConventionPlugin" + } + register("androidLibrary") { + id = "org.fptn.vpn.library.android" + implementationClass = "org.fptn.vpn.gradle.AndroidLibraryConventionPlugin" + } + register("androidLibraryCompose") { + id = "org.fptn.vpn.library.android.compose" + implementationClass = "org.fptn.vpn.gradle.AndroidLibraryComposeConventionPlugin" + } + register("kotlinLibrary") { + id = "org.fptn.vpn.library.kotlin" + implementationClass = "org.fptn.vpn.gradle.KotlinLibraryConventionPlugin" + } + register("koinLibrary") { + id = "org.fptn.vpn.library.koin" + implementationClass = "org.fptn.vpn.gradle.KoinConventionPlugin" + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationComposeConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationComposeConventionPlugin.kt new file mode 100644 index 00000000..30b7e945 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationComposeConventionPlugin.kt @@ -0,0 +1,19 @@ +package org.fptn.vpn.gradle + +import com.android.build.api.dsl.ApplicationExtension +import org.fptn.vpn.gradle.extensions.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +class AndroidApplicationComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + } + val extension = extensions.getByType() + configureAndroidCompose(extension) + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationConventionPlugin.kt new file mode 100644 index 00000000..0318d7be --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationConventionPlugin.kt @@ -0,0 +1,32 @@ +package org.fptn.vpn.gradle + +import com.android.build.api.dsl.ApplicationExtension +import org.fptn.vpn.gradle.extensions.configureAndroidFirebase +import org.fptn.vpn.gradle.extensions.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.extra +import org.gradle.kotlin.dsl.getByType + +class AndroidApplicationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + apply("com.autonomousapps.dependency-analysis") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = rootProject.extra.get("targetSdkVersion") as Int + } + val extension = extensions.getByType() + configureAndroidFirebase(extension) + dependencies { + } + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationKoinConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationKoinConventionPlugin.kt new file mode 100644 index 00000000..f8465775 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidApplicationKoinConventionPlugin.kt @@ -0,0 +1,19 @@ +package org.fptn.vpn.gradle + +import com.android.build.api.dsl.ApplicationExtension +import org.fptn.vpn.gradle.extensions.configureAndroidKoin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +class AndroidApplicationKoinConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + } + val extension = extensions.getByType() + configureAndroidKoin(extension) + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryComposeConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryComposeConventionPlugin.kt new file mode 100644 index 00000000..728f484b --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryComposeConventionPlugin.kt @@ -0,0 +1,19 @@ +package org.fptn.vpn.gradle + +import com.android.build.gradle.LibraryExtension +import org.fptn.vpn.gradle.extensions.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +class AndroidLibraryComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + } + val extension = extensions.getByType() + configureAndroidCompose(extension) + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryConventionPlugin.kt new file mode 100644 index 00000000..53b4345a --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/AndroidLibraryConventionPlugin.kt @@ -0,0 +1,32 @@ +package org.fptn.vpn.gradle + +import com.android.build.gradle.LibraryExtension +import org.fptn.vpn.gradle.extensions.configureAndroidFirebase +import org.fptn.vpn.gradle.extensions.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.extra +import org.gradle.kotlin.dsl.getByType + +class AndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + apply("com.autonomousapps.dependency-analysis") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = rootProject.extra.get("targetSdkVersion") as Int + } + val extension = extensions.getByType() + configureAndroidFirebase(extension) + dependencies { + } + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KoinConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KoinConventionPlugin.kt new file mode 100644 index 00000000..972fdf90 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KoinConventionPlugin.kt @@ -0,0 +1,21 @@ +package org.fptn.vpn.gradle + +import org.fptn.vpn.gradle.extensions.buildLibs +import org.fptn.vpn.gradle.extensions.implementation +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +class KoinConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.google.devtools.ksp") + } + dependencies { + implementation(platform(buildLibs.koin.bom)) + implementation(buildLibs.koin.core) + } + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KotlinLibraryConventionPlugin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KotlinLibraryConventionPlugin.kt new file mode 100644 index 00000000..f75274a8 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/KotlinLibraryConventionPlugin.kt @@ -0,0 +1,18 @@ +package org.fptn.vpn.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +class KotlinLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("kotlin") + apply("com.autonomousapps.dependency-analysis") + } + dependencies { + } + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidCompose.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidCompose.kt new file mode 100644 index 00000000..db85d5e1 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidCompose.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fptn.vpn.gradle.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.File + +/** + * Configure Compose-specific options + */ +internal fun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + project.plugins.apply( + buildLibs.plugins.compose.compiler + .get() + .pluginId, + ) + buildFeatures { + compose = true + } + + dependencies { + implementation(platform(buildLibs.androidx.compose.bom)) + } + } + + tasks.withType().configureEach { + compilerOptions { + for (param in buildComposeMetricsParameters()) { + freeCompilerArgs.add(param) + } + } + } +} + +private fun Project.buildComposeMetricsParameters(): List { + val metricParameters = mutableListOf() + val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") + val enableMetrics = enableMetricsProvider.orNull == "true" + if (enableMetrics) { + val metricsFolder = File(project.buildDir, "compose-metrics") + metricParameters.add("-P") + metricParameters.add( + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath, + ) + } + + val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") + val enableReports = enableReportsProvider.orNull == "true" + if (enableReports) { + val reportsFolder = File(project.buildDir, "compose-reports") + metricParameters.add("-P") + metricParameters.add( + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath, + ) + } + return metricParameters.toList() +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidFirebase.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidFirebase.kt new file mode 100644 index 00000000..9faa0bdc --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidFirebase.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fptn.vpn.gradle.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Configure Firebase-specific options + */ +internal fun Project.configureAndroidFirebase(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + dependencies { + implementation(platform(buildLibs.firebase.bom)) + } + } +} diff --git a/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidKoin.kt b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidKoin.kt new file mode 100644 index 00000000..579fae7d --- /dev/null +++ b/buildSrc/src/main/kotlin/org/fptn/vpn/gradle/extensions/AndroidKoin.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fptn.vpn.gradle.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Configure Firebase-specific options + */ +internal fun Project.configureAndroidKoin(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + dependencies { + implementation(platform(buildLibs.koin.bom)) + implementation(buildLibs.koin.core) + } + } +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index b499d432..0f516e08 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -129,6 +129,7 @@ complexity: LongMethod: active: true threshold: 60 + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] LongParameterList: active: false functionThreshold: 6 @@ -333,6 +334,7 @@ naming: excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] functionPattern: '[a-z][a-zA-Z0-9]*' excludeClassPattern: '$^' + ignoreAnnotated: ['Composable'] FunctionParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 3c68ba6a..ad708f1d 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,6 +1,8 @@ plugins { - id("pvnclient.android.library.kotlin") + id("org.fptn.vpn.library.kotlin") + id("org.fptn.vpn.library.koin") } dependencies { + implementation(libs.kotlinx.coroutines.core) } diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/AppDispatchers.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/AppDispatchers.kt new file mode 100644 index 00000000..28be5249 --- /dev/null +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/AppDispatchers.kt @@ -0,0 +1,7 @@ +package org.fptn.vpn.core.common + +object AppDispatchers { + const val DISPATCHER_DEFAULT = "DispatcherDefault" + const val DISPATCHER_IO = "DispatcherIO" + const val DISPATCHER_UNCONFINED = "DispatcherUnconfined" +} diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/CommonModule.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/CommonModule.kt new file mode 100644 index 00000000..5f90d65c --- /dev/null +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/CommonModule.kt @@ -0,0 +1,20 @@ +package org.fptn.vpn.core.common + +import kotlinx.coroutines.Dispatchers +import org.fptn.vpn.core.common.AppDispatchers.DISPATCHER_DEFAULT +import org.fptn.vpn.core.common.AppDispatchers.DISPATCHER_IO +import org.fptn.vpn.core.common.AppDispatchers.DISPATCHER_UNCONFINED +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val coroutineModule = + module { + single(named(DISPATCHER_DEFAULT)) { Dispatchers.Default } + single(named(DISPATCHER_IO)) { Dispatchers.IO } + single(named(DISPATCHER_UNCONFINED)) { Dispatchers.Unconfined } + } + +val commonModule = + module { + includes(coroutineModule) + } diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt index cf6dca05..125d53d2 100644 --- a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt @@ -2,12 +2,15 @@ package org.fptn.vpn.core.common object Constants { const val APPLICATION_SHARED_PREFERENCES = "fptnvpn-shared-preferences" + const val DATABASE_NAME = "FptnDatabase" + const val DEFAULT_DATA_ID = 0 const val MAIN_NOTIFICATION_CHANNEL_ID = "fptnvpn-notification-main" const val MAIN_NOTIFICATION_CHANNEL_VERSION = "fptnvpn-notification-main-channel-version" const val MAIN_NOTIFICATION_CHANNEL_VERSION_NUM = 2 const val MAIN_NOTIFICATION_CHANNEL_GROUP_ID = "fptnvpn-notification-main-group" const val SELECTED_SERVER: String = "fptn.selected.server" const val SELECTED_SERVER_ID_AUTO: Int = -1 + const val SPACE = " " const val MAIN_CONNECTED_NOTIFICATION_ID = 8975 const val INFO_NOTIFICATION_NOTIFICATION_ID = 8979 const val CURRENT_SNI_SHARED_PREF_KEY: String = "CURRENT_SNI" diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Result.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Result.kt new file mode 100644 index 00000000..62bdabf8 --- /dev/null +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Result.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fptn.vpn.core.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +sealed interface Result { + data class Success( + val data: T, + ) : Result + + data class Error( + val exception: Throwable, + ) : Result + + data object Loading : Result +} + +fun Flow.asResult(): Flow> = + map> { Result.Success(it) } + .onStart { emit(Result.Loading) } + .catch { emit(Result.Error(it)) } diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 00000000..6749fb7e --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.fptn.vpn.library.android") + id("org.fptn.vpn.library.android.compose") +} + +android { + namespace = "org.fptn.vpn.core.designsystem" +} + +dependencies { + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.test.manifest) + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/core/designsystem/proguard-rules.pro b/core/designsystem/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/designsystem/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/icons/PvnIcons.kt b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/icons/PvnIcons.kt new file mode 100644 index 00000000..8120f629 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/icons/PvnIcons.kt @@ -0,0 +1,16 @@ +package org.fptn.vpn.core.designsystem.icons + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.Settings + +object PvnIcons { + val Home = Icons.Rounded.Home + val HomeBorder = Icons.Outlined.Home + val Person = Icons.Rounded.Person + val Settings = Icons.Rounded.Settings + val SettingsBorder = Icons.Outlined.Settings +} diff --git a/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/BackgroundTheme.kt b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/BackgroundTheme.kt new file mode 100644 index 00000000..92776077 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/BackgroundTheme.kt @@ -0,0 +1,17 @@ +package org.fptn.vpn.core.designsystem.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +@Immutable +data class BackgroundTheme( + val color: Color = Color.Unspecified, + val tonalElevation: Dp = Dp.Unspecified, +) + +/** + * A composition local for [BackgroundTheme]. + */ +val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() } diff --git a/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Color.kt new file mode 100644 index 00000000..868da900 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Color.kt @@ -0,0 +1,66 @@ +package org.fptn.vpn.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +@Suppress("MagicNumber") +object Color { + val md_theme_light_primary = Color(0xFF9A4525) + val md_theme_light_onPrimary = Color(0xFFFFFFFF) + val md_theme_light_primaryContainer = Color(0xFFFFDBCF) + val md_theme_light_onPrimaryContainer = Color(0xFF390C00) + val md_theme_light_secondary = Color(0xFF77574C) + val md_theme_light_onSecondary = Color(0xFFFFFFFF) + val md_theme_light_secondaryContainer = Color(0xFFFFDBCF) + val md_theme_light_onSecondaryContainer = Color(0xFF2C160E) + val md_theme_light_tertiary = Color(0xFF6A5E2F) + val md_theme_light_onTertiary = Color(0xFFFFFFFF) + val md_theme_light_tertiaryContainer = Color(0xFFF3E2A7) + val md_theme_light_onTertiaryContainer = Color(0xFF221B00) + val md_theme_light_error = Color(0xFFBA1A1A) + val md_theme_light_errorContainer = Color(0xFFFFDAD6) + val md_theme_light_onError = Color(0xFFFFFFFF) + val md_theme_light_onErrorContainer = Color(0xFF410002) + val md_theme_light_background = Color(0xFFFFFBFF) + val md_theme_light_onBackground = Color(0xFF201A18) + val md_theme_light_surface = Color(0xFFFFFBFF) + val md_theme_light_onSurface = Color(0xFF201A18) + val md_theme_light_surfaceVariant = Color(0xFFF5DED7) + val md_theme_light_onSurfaceVariant = Color(0xFF53433E) + val md_theme_light_outline = Color(0xFF85736D) + val md_theme_light_inverseOnSurface = Color(0xFFFBEEEA) + val md_theme_light_inverseSurface = Color(0xFF362F2D) + val md_theme_light_inversePrimary = Color(0xFFFFB59C) + val md_theme_light_shadow = Color(0xFF000000) + val md_theme_light_surfaceTint = Color(0xFF9A4525) + val md_theme_light_surfaceTintColor = Color(0xFF9A4525) + val seed = Color(0xFFfedbd0) + val md_theme_dark_primary = Color(0xFFFFB59C) + val md_theme_dark_onPrimary = Color(0xFF5C1900) + val md_theme_dark_primaryContainer = Color(0xFF7C2E0F) + val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBCF) + val md_theme_dark_secondary = Color(0xFFE7BDB0) + val md_theme_dark_onSecondary = Color(0xFF442A21) + val md_theme_dark_secondaryContainer = Color(0xFF5D4036) + val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBCF) + val md_theme_dark_tertiary = Color(0xFFD6C68D) + val md_theme_dark_onTertiary = Color(0xFF393005) + val md_theme_dark_tertiaryContainer = Color(0xFF51461A) + val md_theme_dark_onTertiaryContainer = Color(0xFFF3E2A7) + val md_theme_dark_error = Color(0xFFFFB4AB) + val md_theme_dark_errorContainer = Color(0xFF93000A) + val md_theme_dark_onError = Color(0xFF690005) + val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) + val md_theme_dark_background = Color(0xFF201A18) + val md_theme_dark_onBackground = Color(0xFFEDE0DC) + val md_theme_dark_surface = Color(0xFF201A18) + val md_theme_dark_onSurface = Color(0xFFEDE0DC) + val md_theme_dark_surfaceVariant = Color(0xFF53433E) + val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BB) + val md_theme_dark_outline = Color(0xFFA08D87) + val md_theme_dark_inverseOnSurface = Color(0xFF201A18) + val md_theme_dark_inverseSurface = Color(0xFFEDE0DC) + val md_theme_dark_inversePrimary = Color(0xFF9A4525) + val md_theme_dark_shadow = Color(0xFF000000) + val md_theme_dark_surfaceTint = Color(0xFFFFB59C) + val md_theme_dark_surfaceTintColor = Color(0xFFFFB59C) +} diff --git a/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/PvnTheme.kt b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/PvnTheme.kt new file mode 100644 index 00000000..b0b58b18 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/PvnTheme.kt @@ -0,0 +1,110 @@ +package org.fptn.vpn.core.designsystem.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val LightColorScheme = + lightColorScheme( + primary = Color.md_theme_light_primary, + onPrimary = Color.md_theme_light_onPrimary, + primaryContainer = Color.md_theme_light_primaryContainer, + onPrimaryContainer = Color.md_theme_light_onPrimaryContainer, + secondary = Color.md_theme_light_secondary, + onSecondary = Color.md_theme_light_onSecondary, + secondaryContainer = Color.md_theme_light_secondaryContainer, + onSecondaryContainer = Color.md_theme_light_onSecondaryContainer, + tertiary = Color.md_theme_light_tertiary, + onTertiary = Color.md_theme_light_onTertiary, + tertiaryContainer = Color.md_theme_light_tertiaryContainer, + onTertiaryContainer = Color.md_theme_light_onTertiaryContainer, + background = Color.md_theme_light_background, + onBackground = Color.md_theme_light_onBackground, + surface = Color.md_theme_light_surface, + onSurface = Color.md_theme_light_onSurface, + surfaceVariant = Color.md_theme_light_surfaceVariant, + onSurfaceVariant = Color.md_theme_light_onSurfaceVariant, + surfaceTint = Color.md_theme_light_surfaceTint, + inverseSurface = Color.md_theme_light_inverseSurface, + inverseOnSurface = Color.md_theme_light_inverseOnSurface, + error = Color.md_theme_light_error, + onError = Color.md_theme_light_onError, + errorContainer = Color.md_theme_light_errorContainer, + onErrorContainer = Color.md_theme_light_onErrorContainer, + outline = Color.md_theme_light_outline, + ) +private val DarkColorScheme = + darkColorScheme( + primary = Color.md_theme_dark_primary, + onPrimary = Color.md_theme_dark_onPrimary, + primaryContainer = Color.md_theme_dark_primaryContainer, + onPrimaryContainer = Color.md_theme_dark_onPrimaryContainer, + secondary = Color.md_theme_dark_secondary, + onSecondary = Color.md_theme_dark_onSecondary, + secondaryContainer = Color.md_theme_dark_secondaryContainer, + onSecondaryContainer = Color.md_theme_dark_onSecondaryContainer, + tertiary = Color.md_theme_dark_tertiary, + onTertiary = Color.md_theme_dark_onTertiary, + tertiaryContainer = Color.md_theme_dark_tertiaryContainer, + onTertiaryContainer = Color.md_theme_dark_onTertiaryContainer, + background = Color.md_theme_dark_background, + onBackground = Color.md_theme_dark_onBackground, + surface = Color.md_theme_dark_surface, + onSurface = Color.md_theme_dark_onSurface, + surfaceVariant = Color.md_theme_dark_surfaceVariant, + onSurfaceVariant = Color.md_theme_dark_onSurfaceVariant, + surfaceTint = Color.md_theme_dark_surfaceTint, + inverseSurface = Color.md_theme_dark_inverseSurface, + inverseOnSurface = Color.md_theme_dark_inverseOnSurface, + error = Color.md_theme_dark_error, + onError = Color.md_theme_dark_onError, + errorContainer = Color.md_theme_dark_errorContainer, + onErrorContainer = Color.md_theme_dark_onErrorContainer, + outline = Color.md_theme_dark_outline, + ) + +@Composable +fun PvnTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = false, + content: @Composable () -> Unit, +) { + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> { + DarkColorScheme + } + else -> { + LightColorScheme + } + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.background.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + MaterialTheme( + colorScheme = colorScheme, + typography = MmTypography, + content = content, + ) +} diff --git a/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Type.kt b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Type.kt new file mode 100644 index 00000000..a51ef623 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/fptn/vpn/core/designsystem/theme/Type.kt @@ -0,0 +1,118 @@ +package org.fptn.vpn.core.designsystem.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +/** + * Now in Android typography. + */ +internal val MmTypography = + Typography( + displayLarge = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + titleLarge = + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + ), + titleSmall = + TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodyMedium = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + labelLarge = + TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = + TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 16.sp, + letterSpacing = 0.sp, + ), + ) diff --git a/core/designsystem/src/main/res/drawable/ic_launcher_background.xml b/core/designsystem/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/values/colors.xml b/core/designsystem/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/core/designsystem/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/core/designsystem/src/main/res/values/dimens.xml b/core/designsystem/src/main/res/values/dimens.xml new file mode 100644 index 00000000..ec08bbdc --- /dev/null +++ b/core/designsystem/src/main/res/values/dimens.xml @@ -0,0 +1,8 @@ + + + 64dp + 32dp + 16dp + 8dp + 4dp + \ No newline at end of file diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml new file mode 100644 index 00000000..c7fbb195 --- /dev/null +++ b/core/designsystem/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + designsystem + \ No newline at end of file diff --git a/core/model/.gitignore b/core/model/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/model/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts new file mode 100644 index 00000000..6350cade --- /dev/null +++ b/core/model/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("org.fptn.vpn.library.kotlin") + alias(libs.plugins.serialization) +} + +dependencies { + implementation(libs.kotlinx.serialization.core.jvm) +} diff --git a/core/model/proguard-rules.pro b/core/model/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/model/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUser.kt b/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUser.kt new file mode 100644 index 00000000..91f48c05 --- /dev/null +++ b/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUser.kt @@ -0,0 +1,40 @@ +package org.fptn.vpn.core.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FptnUser( + @SerialName("version") val version: Int, + @SerialName("service_name") val serviceName: String, + @SerialName("username") val username: String, + @SerialName("password") val password: String, + @SerialName("servers") val servers: List = emptyList(), + @SerialName("censored_zone_servers") val censoredZoneServers: List = emptyList(), +) + +@Serializable +data class FptnServer( + @SerialName("name") val name: String, + @SerialName("host") val host: String, + @SerialName("md5_fingerprint") val md5Fingerprint: String, + @SerialName("port") val port: Int, +) + +fun FptnUser.toDomain() = + FptnUserDomain( + version = version, + serviceName = serviceName, + username = username, + password = password, + servers = servers.map { it.toDomain() }, + censoredZoneServers = censoredZoneServers.map { it.toDomain() }, + ) + +fun FptnServer.toDomain() = + FptnServerDomain( + name = name, + host = host, + md5Fingerprint = md5Fingerprint, + port = port, + ) diff --git a/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUserDomain.kt b/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUserDomain.kt new file mode 100644 index 00000000..dc1694c3 --- /dev/null +++ b/core/model/src/main/kotlin/org/fptn/vpn/core/model/FptnUserDomain.kt @@ -0,0 +1,17 @@ +package org.fptn.vpn.core.model + +data class FptnUserDomain( + val version: Int, + val serviceName: String, + val username: String, + val password: String, + val servers: List = emptyList(), + val censoredZoneServers: List = emptyList(), +) + +data class FptnServerDomain( + val name: String, + val host: String, + val md5Fingerprint: String, + val port: Int, +) diff --git a/core/network/.gitignore b/core/network/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 00000000..e54696cb --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("org.fptn.vpn.library.android") + id("org.fptn.vpn.library.koin") +} + +android { + namespace = "org.fptn.vpn.core.network" +} + +dependencies { + implementation(libs.androidx.tracing) + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) +} diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/network/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d7546021 --- /dev/null +++ b/core/network/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/core/network/src/main/kotlin/org/fptn/vpn/core/network/ConnectivityManagerNetworkMonitor.kt b/core/network/src/main/kotlin/org/fptn/vpn/core/network/ConnectivityManagerNetworkMonitor.kt new file mode 100644 index 00000000..942f0756 --- /dev/null +++ b/core/network/src/main/kotlin/org/fptn/vpn/core/network/ConnectivityManagerNetworkMonitor.kt @@ -0,0 +1,77 @@ +package org.fptn.vpn.core.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.NetworkRequest.Builder +import androidx.tracing.trace +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn + +internal class ConnectivityManagerNetworkMonitor( + private val context: Context, + ioDispatcher: CoroutineDispatcher, +) : NetworkMonitor { + @Suppress("LabeledExpression") + override val isOnline: Flow = + callbackFlow { + trace("NetworkMonitor.callbackFlow") { + val connectivityManager = context.getSystemService("ftpn.vpn") as? ConnectivityManager + if (connectivityManager == null) { + channel.trySend(false) + channel.close() + return@callbackFlow + } + + /** + * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], + * not just the active network. So we can simply track the presence (or absence) of such [Network]. + */ + val callback = + object : NetworkCallback() { + private val networks = mutableSetOf() + + override fun onAvailable(network: Network) { + networks += network + channel.trySend(true) + } + + override fun onLost(network: Network) { + networks -= network + channel.trySend(networks.isNotEmpty()) + } + } + + trace("NetworkMonitor.registerNetworkCallback") { + val request = + Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, callback) + } + + /** + * Sends the latest connectivity status to the underlying channel. + */ + channel.trySend(connectivityManager.isCurrentlyConnected()) + + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + } + }.flowOn(ioDispatcher) + .conflate() + + @Suppress("DEPRECATION") + private fun ConnectivityManager.isCurrentlyConnected(): Boolean = + activeNetwork + ?.let(::getNetworkCapabilities) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false +} diff --git a/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkModule.kt b/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkModule.kt new file mode 100644 index 00000000..0ecad0bf --- /dev/null +++ b/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkModule.kt @@ -0,0 +1,8 @@ +package org.fptn.vpn.core.network + +import org.koin.dsl.module + +val networkModule = + module { + single { ConnectivityManagerNetworkMonitor(get(), get()) } + } diff --git a/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkMonitor.kt b/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkMonitor.kt new file mode 100644 index 00000000..ffae2156 --- /dev/null +++ b/core/network/src/main/kotlin/org/fptn/vpn/core/network/NetworkMonitor.kt @@ -0,0 +1,7 @@ +package org.fptn.vpn.core.network + +import kotlinx.coroutines.flow.Flow + +interface NetworkMonitor { + val isOnline: Flow +} diff --git a/core/persistent/.gitignore b/core/persistent/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/persistent/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/persistent/build.gradle.kts b/core/persistent/build.gradle.kts new file mode 100644 index 00000000..210c0130 --- /dev/null +++ b/core/persistent/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("org.fptn.vpn.library.android") + id("org.fptn.vpn.library.koin") +} + +android { + namespace = "org.fptn.vpn.core.persistent" +} + +dependencies { + implementation(project(":core:common")) + implementation(project(":core:model")) + implementation(libs.androidx.room.runtime) + implementation(libs.datastore) + implementation(libs.koin.android) + implementation(libs.koin.core) + + ksp(libs.androidx.room.compiler) +} diff --git a/core/persistent/proguard-rules.pro b/core/persistent/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/persistent/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/AppDatabase.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/AppDatabase.kt new file mode 100644 index 00000000..93153daf --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/AppDatabase.kt @@ -0,0 +1,16 @@ +package org.fptn.vpn.core.persistent + +import androidx.room.Database +import androidx.room.RoomDatabase +import org.fptn.vpn.core.persistent.AppDatabase.Companion.VERSION_NUMBER +import org.fptn.vpn.core.persistent.model.FptnServerDao +import org.fptn.vpn.core.persistent.model.FptnServerDbModel + +@Database(entities = [FptnServerDbModel::class], version = VERSION_NUMBER, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun serverDao(): FptnServerDao + + companion object { + const val VERSION_NUMBER = 10 + } +} diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/PreferencesStore.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/PreferencesStore.kt new file mode 100644 index 00000000..d3a5e2fa --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/PreferencesStore.kt @@ -0,0 +1,41 @@ +package org.fptn.vpn.core.persistent + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val DATA_STORE_NAME = "PvnDataPreferencesStore" +private const val BASE_NAMESPACE = "org.fptn.vpn" +const val PREFERENCE_CLIENT_UID = BASE_NAMESPACE + "current.client.uid" +private val PREFERENCE_CLIENT_TOKEN_KEY = stringPreferencesKey(PREFERENCE_CLIENT_UID) + +val Context.dataStore: DataStore by preferencesDataStore(DATA_STORE_NAME) + +interface PreferenceStore { + val token: Flow + + suspend fun updateToken(token: String) + + suspend fun clearAllData() +} + +class PreferencesStoreImpl( + context: Context, +) : PreferenceStore { + private val dataStore: DataStore = context.dataStore + + override val token: Flow = dataStore.data.map { data -> data[PREFERENCE_CLIENT_TOKEN_KEY].orEmpty() } + + override suspend fun updateToken(token: String) { + dataStore.edit { preferences -> preferences[PREFERENCE_CLIENT_TOKEN_KEY] = token } + } + + override suspend fun clearAllData() { + dataStore.edit { it.clear() } + } +} diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/DatabaseModule.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/DatabaseModule.kt new file mode 100644 index 00000000..12f45f5a --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/DatabaseModule.kt @@ -0,0 +1,20 @@ +package org.fptn.vpn.core.persistent.di + +import androidx.room.Room +import androidx.room.RoomDatabase +import org.fptn.vpn.core.common.Constants.DATABASE_NAME +import org.fptn.vpn.core.persistent.AppDatabase +import org.fptn.vpn.core.persistent.model.FptnServerDao +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val databaseModule = + module { + single { + Room + .databaseBuilder(androidContext(), AppDatabase::class.java, DATABASE_NAME) + .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) + .build() + } + single { get().serverDao() } + } diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/PersistentModule.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/PersistentModule.kt new file mode 100644 index 00000000..2f557682 --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/di/PersistentModule.kt @@ -0,0 +1,10 @@ +package org.fptn.vpn.core.persistent.di + +import org.fptn.vpn.core.persistent.PreferenceStore +import org.fptn.vpn.core.persistent.PreferencesStoreImpl +import org.koin.dsl.module + +val persistentModule = + module { + single { PreferencesStoreImpl(get()) } + } diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDao.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDao.kt new file mode 100644 index 00000000..a5b16a01 --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDao.kt @@ -0,0 +1,10 @@ +package org.fptn.vpn.core.persistent.model + +import androidx.room.Dao +import androidx.room.Upsert + +@Dao +interface FptnServerDao { + @Upsert + suspend fun insert(user: FptnServerDbModel) +} diff --git a/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDbModel.kt b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDbModel.kt new file mode 100644 index 00000000..340df687 --- /dev/null +++ b/core/persistent/src/main/kotlin/org/fptn/vpn/core/persistent/model/FptnServerDbModel.kt @@ -0,0 +1,39 @@ +package org.fptn.vpn.core.persistent.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.fptn.vpn.core.common.Constants.DEFAULT_DATA_ID +import org.fptn.vpn.core.model.FptnServerDomain + +@Entity(tableName = "server_table") +data class FptnServerDbModel( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") val id: Int, + @ColumnInfo(name = "isSelected") val isSelected: Boolean, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "username") val username: String, + @ColumnInfo(name = "password") val password: String, + @ColumnInfo(name = "host") val host: String, + @ColumnInfo(name = "port") val port: Int, + @ColumnInfo(name = "countryCode") val countryCode: String, + @ColumnInfo(name = "md5ServerFingerprint") val md5ServerFingerprint: String, + @ColumnInfo(name = "censured") val censured: Boolean, +) + +fun FptnServerDomain.toDbModel( + username: String, + password: String, + censured: Boolean, +) = FptnServerDbModel( + id = DEFAULT_DATA_ID, + isSelected = false, + name = name, + username = username, + password = password, + host = host, + port = port, + countryCode = "US", + md5ServerFingerprint = md5Fingerprint, + censured = censured, +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9875be18..500f578c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,14 @@ [versions] activity = "1.10.1" +androidxComposeBom="2025.06.01" appcompat = "1.7.1" autonomousappsSorting = "0.14" autonomousappsUnused = "2.19.0" +composeConstraintLayout = "1.1.1" constraintlayout = "2.2.1" +coroutinesCore = "1.10.2" crashlytics = "3.0.5" +datastore = "1.1.7" detektPlugin = "1.23.8" firebaseBom = "34.0.0" firebaseCrashlyticsPlugin = "3.0.5" @@ -14,27 +18,52 @@ guava = "33.4.8-jre" ipaddress = "5.5.1" jacksonDatabind = "2.19.2" java = "17" +javaPoet = "1.13.0" junit = "4.13.2" junitTest = "1.2.1" +koin = "4.1.0" +koinAnnotations = "2.1.0" kotlin = "2.2.0" ksp = "2.2.0-2.0.2" lombock = "1.18.38" material = "1.12.0" +mockk = "1.14.5" monitor = "1.7.2" +navigation = "2.9.1" protobuf = "4.26.1" roomRuntime = "2.7.2" +serialization = "1.9.0" +splashScreen = "1.0.1" +timber = "5.0.1" +tracing = "1.3.0" zxing = "3.5.3" [libraries] android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "gradlePlugin" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "composeConstraintLayout" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitTest" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } +androidx-navigation-runtime-android = { group = "androidx.navigation", name = "navigation-runtime-android", version.ref = "navigation" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } androidx-room-guava = { module = "androidx.room:room-guava", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } +androidx-tracing = { group = "androidx.tracing", name = "tracing", version.ref = "tracing" } +datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } detekt = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detektPlugin" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } @@ -44,16 +73,33 @@ google-services = { group = "com.google.gms", name = "google-services", version. guava = { module = "com.google.guava:guava", version.ref = "guava" } ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = "ipaddress" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jacksonDatabind" } +java-poet = { group = "com.squareup", name = "javapoet", version.ref = "javaPoet" } junit = { group = "junit", name = "junit", version.ref = "junit" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose" } +koin-androidx-runtime-android = { group = "io.insert-koin", name = "koin-androidx-runtime-android" } +koin-annotations-jvm = { group = "io.insert-koin", name = "koin-annotations-jvm", version.ref = "koinAnnotations" } +koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } +koin-core = { group = "io.insert-koin", name = "koin-core" } +koin-core-viewmodel-jvm = { group = "io.insert-koin", name = "koin-core-viewmodel-jvm" } +koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-metadata-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-metadata-jvm", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutinesCore" } +kotlinx-serialization-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core-jvm", version.ref = "serialization" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } lombock = { group = "org.projectlombok", name = "lombok", version.ref = "lombock" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } zxing = { module = "com.google.zxing:core", version.ref = "zxing" } [plugins] +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" } deps-sorting = { id = "com.squareup.sort-dependencies", version.ref = "autonomousappsSorting" } deps-unused = { id = "com.autonomousapps.dependency-analysis", version.ref = "autonomousappsUnused" } - +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" } diff --git a/home/data/.gitignore b/home/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/home/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/home/data/build.gradle.kts b/home/data/build.gradle.kts new file mode 100644 index 00000000..6e3edd5a --- /dev/null +++ b/home/data/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("org.fptn.vpn.library.android") +} + +android { + namespace = "org.fptn.vpn.home.data" +} + +dependencies { +} diff --git a/home/data/proguard-rules.pro b/home/data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/home/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/home/domain/.gitignore b/home/domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/home/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/home/domain/build.gradle.kts b/home/domain/build.gradle.kts new file mode 100644 index 00000000..17358ddb --- /dev/null +++ b/home/domain/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + id("org.fptn.vpn.library.kotlin") +} + +dependencies { +} diff --git a/home/domain/proguard-rules.pro b/home/domain/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/home/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/home/ui/.gitignore b/home/ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/home/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/home/ui/build.gradle.kts b/home/ui/build.gradle.kts new file mode 100644 index 00000000..b46a22f5 --- /dev/null +++ b/home/ui/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("org.fptn.vpn.library.android") +} + +android { + namespace = "org.fptn.vpn.home.ui" +} + +dependencies { + implementation(libs.androidx.navigation.runtime.android) +} diff --git a/home/ui/proguard-rules.pro b/home/ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/home/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/home/ui/src/main/kotlin/com/filantrop/pvnclient/home/ui/navigation/HomeNavigation.kt b/home/ui/src/main/kotlin/com/filantrop/pvnclient/home/ui/navigation/HomeNavigation.kt new file mode 100644 index 00000000..08ce863b --- /dev/null +++ b/home/ui/src/main/kotlin/com/filantrop/pvnclient/home/ui/navigation/HomeNavigation.kt @@ -0,0 +1,14 @@ +package com.filantrop.pvnclient.home.ui.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavOptions + +data object HomeRoute // route to Home screen + +const val HOME_ROUTE = "homeRoute" + +data object HomeBaseRoute // route to base navigation graph + +const val HOME_BASE_ROUTE = "homeRouteStr" + +fun NavController.navigateToHome(navOptions: NavOptions) = navigate(route = HomeRoute, navOptions) diff --git a/settings.gradle.kts b/settings.gradle.kts index 546ea2eb..9404eecb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,5 +9,18 @@ pluginManagement { } include(":app") +include(":auth:data") +include(":auth:domain") +include(":auth:ui") include(":core:common") +include(":core:designsystem") +include(":core:model") +include(":core:network") +include(":core:persistent") +include(":home:data") +include(":home:domain") +include(":home:ui") +include(":settings:data") +include(":settings:domain") +include(":settings:ui") include(":vpnclient") diff --git a/settings/data/.gitignore b/settings/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/settings/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/settings/data/build.gradle.kts b/settings/data/build.gradle.kts new file mode 100644 index 00000000..d73a179e --- /dev/null +++ b/settings/data/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("org.fptn.vpn.library.android") +} + +android { + namespace = "org.fptn.vpn.settings.data" +} + +dependencies { +} diff --git a/settings/data/proguard-rules.pro b/settings/data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/settings/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/settings/domain/.gitignore b/settings/domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/settings/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/settings/domain/build.gradle.kts b/settings/domain/build.gradle.kts new file mode 100644 index 00000000..17358ddb --- /dev/null +++ b/settings/domain/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + id("org.fptn.vpn.library.kotlin") +} + +dependencies { +} diff --git a/settings/domain/proguard-rules.pro b/settings/domain/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/settings/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/settings/ui/.gitignore b/settings/ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/settings/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/settings/ui/build.gradle.kts b/settings/ui/build.gradle.kts new file mode 100644 index 00000000..a1316926 --- /dev/null +++ b/settings/ui/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("org.fptn.vpn.library.android") +} + +android { + namespace = "org.fptn.vpn.settings.ui" +} + +dependencies { + implementation(libs.androidx.navigation.runtime.android) +} diff --git a/settings/ui/proguard-rules.pro b/settings/ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/settings/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/settings/ui/src/main/kotlin/com/filantrop/pvnclient/settings/ui/navigation/SettingsRoute.kt b/settings/ui/src/main/kotlin/com/filantrop/pvnclient/settings/ui/navigation/SettingsRoute.kt new file mode 100644 index 00000000..8177c477 --- /dev/null +++ b/settings/ui/src/main/kotlin/com/filantrop/pvnclient/settings/ui/navigation/SettingsRoute.kt @@ -0,0 +1,10 @@ +package com.filantrop.pvnclient.settings.ui.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavOptions + +data object SettingsRoute // route to Settings screen + +const val SETTINGS_ROUTE = "settingsRoute" + +fun NavController.navigateToSettings(navOptions: NavOptions) = navigate(route = SettingsRoute, navOptions) diff --git a/vpnclient/build.gradle.kts b/vpnclient/build.gradle.kts index 3c68ba6a..17358ddb 100644 --- a/vpnclient/build.gradle.kts +++ b/vpnclient/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("pvnclient.android.library.kotlin") + id("org.fptn.vpn.library.kotlin") } dependencies {