Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
app/.classpath
app/.project
app/release
auth/
#core/
.idea/
.project
Expand Down
20 changes: 18 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
30 changes: 15 additions & 15 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,50 +20,50 @@
tools:node="remove" />

<application
android:name=".PvnApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.FptnClient"
tools:targetApi="33">
<activity
android:screenOrientation="portrait"
android:name="org.fptn.vpn.views.SplashActivity"
android:theme="@style/Theme.RemoveSplashScreenTheme"
android:name=".ui.MainActivity"
android:exported="true"
android:noHistory="true"
android:launchMode="singleTop"
android:label="@string/app_name">
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/Theme.FptnClient.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:screenOrientation="portrait"
android:name="org.fptn.vpn.views.LoginActivity"
android:noHistory="true"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:label="@string/app_name" />
android:noHistory="true"
android:screenOrientation="portrait" />
<activity
android:screenOrientation="portrait"
android:name="org.fptn.vpn.views.HomeActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:label="@string/app_name" />
android:screenOrientation="portrait" />
<activity
android:screenOrientation="portrait"
android:name="org.fptn.vpn.views.SettingsActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:label="@string/app_name" />
android:screenOrientation="portrait" />
<activity
android:screenOrientation="portrait"
android:name="org.fptn.vpn.views.SettingsActivityUpdateToken"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:label="@string/app_name" />
android:screenOrientation="portrait" />

<service
android:name="org.fptn.vpn.services.CustomVpnService"
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/kotlin/org/fptn/vpn/PvnApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.fptn.vpn

import android.app.Application
import org.fptn.vpn.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class PvnApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@PvnApplication)
modules(appModule)
}
}
}
28 changes: 28 additions & 0 deletions app/src/main/kotlin/org/fptn/vpn/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.fptn.vpn.di

import com.filantrop.pvnclient.auth.ui.authModule
import org.fptn.vpn.core.common.commonModule
import org.fptn.vpn.core.network.networkModule
import org.fptn.vpn.core.persistent.di.databaseModule
import org.fptn.vpn.core.persistent.di.persistentModule
import org.fptn.vpn.viewmodel.MainViewModel
import org.koin.core.module.Module
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module

val viewModelModule =
module {
viewModel { MainViewModel(get()) }
}

val appModule: Module =
module {
includes(
authModule,
commonModule,
persistentModule,
databaseModule,
networkModule,
viewModelModule,
)
}
34 changes: 34 additions & 0 deletions app/src/main/kotlin/org/fptn/vpn/navigation/TopLevelDestination.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.fptn.vpn.navigation

import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector
import com.filantrop.pvnclient.home.ui.navigation.HOME_BASE_ROUTE
import com.filantrop.pvnclient.home.ui.navigation.HOME_ROUTE
import com.filantrop.pvnclient.settings.ui.navigation.SETTINGS_ROUTE
import org.fptn.vpn.R
import org.fptn.vpn.core.designsystem.icons.PvnIcons

enum class TopLevelDestination(
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector,
@StringRes val iconTextId: Int,
@StringRes val titleTextId: Int,
val route: String,
val baseRoute: String = route,
) {
HOME(
selectedIcon = PvnIcons.Home,
unselectedIcon = PvnIcons.HomeBorder,
iconTextId = R.string.home_title,
titleTextId = R.string.home_title,
route = HOME_ROUTE,
baseRoute = HOME_BASE_ROUTE,
),
SETTINGS(
selectedIcon = PvnIcons.Settings,
unselectedIcon = PvnIcons.SettingsBorder,
iconTextId = R.string.settings_title,
titleTextId = R.string.settings_title,
route = SETTINGS_ROUTE,
),
}
84 changes: 84 additions & 0 deletions app/src/main/kotlin/org/fptn/vpn/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.fptn.vpn.ui

import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.filantrop.pvnclient.auth.ui.AuthScreen
import kotlinx.coroutines.launch
import org.fptn.vpn.core.designsystem.theme.PvnTheme
import org.fptn.vpn.viewmodel.AuthActivityUiState
import org.fptn.vpn.viewmodel.MainViewModel
import org.fptn.vpn.views.HomeActivity
import org.koin.androidx.viewmodel.ext.android.viewModel

class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)

lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { openScreen(it) }
}
}
splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
}

private fun openScreen(state: AuthActivityUiState) {
setContent {
val darkTheme = isSystemInDarkTheme()

// Update the edge to edge configuration to match the theme
// This is the same parameters as the default enableEdgeToEdge call, but we manually
// resolve whether or not to show dark theme using uiState, since it can be different
// than the configuration's dark theme value based on the user preference.
DisposableEffect(darkTheme) {
enableEdgeToEdge(
statusBarStyle =
SystemBarStyle.auto(
Color.TRANSPARENT,
Color.TRANSPARENT,
) { darkTheme },
navigationBarStyle =
SystemBarStyle.auto(
lightScrim,
darkScrim,
) { darkTheme },
)
onDispose {}
}
PvnTheme(darkTheme = darkTheme) {
when (state) {
is AuthActivityUiState.Loading -> {
}
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)
50 changes: 50 additions & 0 deletions app/src/main/kotlin/org/fptn/vpn/viewmodel/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<AuthActivityUiState> =
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
}
34 changes: 34 additions & 0 deletions app/src/main/res/drawable/ic_splash.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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

http://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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">

<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="@color/ic_launcher_background_tint"/>
<path
android:pathData="M65.08,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM43.6,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM65.77,72.44 L69.66,65.73a0.81,0.81 0,0 0,-0.3 -1.1,0.82 0.82,0 0,0 -1.11,0.3l-3.93,6.8a24,24 0,0 0,-9.99 -2.14c-3.6,0 -6.98,0.77 -9.99,2.14l-3.93,-6.8a0.8,0.8 0,1 0,-1.4 0.8l3.88,6.71A22.91,22.91 0,0 0,31 90.77h46.67a22.9,22.9 0,0 0,-11.9 -18.33Z"
android:fillColor="@color/ic_launcher_foreground_tint"/>
<path
android:pathData="M46.57,35a0.85,0.85 0,0 0,-0.85 0.85v7.3h-1.53a1.52,1.52 0,0 0,0 3.05h1.53v-3.05h1.7c0.75,0 1.36,-0.61 1.36,-1.36v-4.07h1.19c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.85h-3.4ZM46.57,54.35h3.4c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.84h-1.19v-4.08c0,-0.75 -0.61,-1.36 -1.36,-1.36h-1.7v7.3c0,0.47 0.38,0.85 0.85,0.85ZM61.54,35c0.47,0 0.85,0.38 0.85,0.85v7.3h1.53a1.52,1.52 0,0 1,0 3.05h-1.53v-3.05h-1.7c-0.75,0 -1.36,-0.61 -1.36,-1.36v-4.07h-1.18a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.47 0.38,-0.85 0.85,-0.85h3.39ZM61.54,54.35h-3.39a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.46 0.38,-0.84 0.85,-0.84h1.18v-4.08c0,-0.75 0.61,-1.36 1.36,-1.36h1.7v7.3c0,0.47 -0.38,0.85 -0.85,0.85Z"
android:fillColor="@color/ic_launcher_foreground_tint"
android:fillType="evenOdd"/>

</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/values-night/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background_tint">#FCFCFC</color>
<color name="ic_launcher_foreground_tint">#000000</color>
</resources>
Loading
Loading