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
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.interpolator)
debugImplementation(libs.androidx.ui.tooling)

// Navigation
Expand All @@ -72,4 +73,7 @@ dependencies {

// Coroutines
implementation(libs.kotlinx.coroutines.android)

//Splash Screen
implementation(libs.androidx.core.splashscreen)
}
15 changes: 5 additions & 10 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Network & VPN Permissions -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
Expand All @@ -20,18 +19,15 @@
android:supportsRtl="true"
android:theme="@style/Theme.AppFence">

<!-- Main Activity -->
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.AppFence">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
android:theme="@style/Theme.AppFence.Splash"> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- VPN Service -->
<service
android:name=".vpn.AppFenceVpnService"
android:exported="false"
Expand All @@ -45,7 +41,6 @@
android:value="true" />
</service>

<!-- Boot Receiver -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
Expand All @@ -58,4 +53,4 @@

</application>

</manifest>
</manifest>
Binary file added app/src/main/ic_launcher-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 40 additions & 39 deletions app/src/main/java/com/yogesh/appfence/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.yogesh.appfence.ui

import android.app.Activity
import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import android.widget.Toast
Expand All @@ -13,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand All @@ -23,7 +23,6 @@ import com.yogesh.appfence.ui.screens.SettingsScreen
import com.yogesh.appfence.ui.theme.AppFenceTheme
import com.yogesh.appfence.ui.viewmodel.MainViewModel
import com.yogesh.appfence.ui.viewmodel.SettingsViewModel
import com.yogesh.appfence.vpn.NetworkType

/**
* Single-activity host for the entire Compose UI.
Expand All @@ -42,7 +41,6 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// Permission granted β€” start VPN and complete onboarding
settingsViewModel.startVpn()
settingsViewModel.completeOnboarding()
} else {
Expand All @@ -55,54 +53,58 @@ class MainActivity : ComponentActivity() {
}

override fun onCreate(savedInstanceState: Bundle?) {

val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()

mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]

@yogesh-7 yogesh-7 May 1, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate ViewModel initialization in MainActivity.kt

The PR correctly moves mainViewModel and settingsViewModel initialization earlier (to use them in setKeepOnScreenCondition), but the original initialization lines further down in onCreate were not removed.

As a result, both ViewModels are being constructed twice on every cold start.

Fix:

Remove the duplicate initialization lines (the ones in the original position):

mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]

settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]


splashScreen.setKeepOnScreenCondition { false }

enableEdgeToEdge()

setContent {
AppFenceTheme {
val navController = rememberNavController()
val onboardingCompleted by settingsViewModel.onboardingCompleted.collectAsState()
val networkType by settingsViewModel.networkMonitor.networkType.collectAsState()
val isLoading by mainViewModel.isLoading.collectAsState()

val startDestination = if (onboardingCompleted) "main" else "onboarding"

NavHost(
navController = navController,
startDestination = startDestination,
modifier = Modifier.fillMaxSize()
) {
composable("onboarding") {
OnboardingScreen(
onGrantVpnPermission = { requestVpnPermission() }
)
}
if (isLoading) {
com.yogesh.appfence.ui.screens.AppFenceSplashScreen()
} else {
val startDestination = if (onboardingCompleted) "main" else "onboarding"

composable("main") {
MainScreen(
viewModel = mainViewModel,
networkType = networkType,
onNavigateToSettings = {
navController.navigate("settings")
}
)
NavHost(
navController = navController,
startDestination = startDestination,
modifier = Modifier.fillMaxSize()
) {
composable("onboarding") {
OnboardingScreen(onGrantVpnPermission = { requestVpnPermission() })
}
composable("main") {
MainScreen(
viewModel = mainViewModel,
networkType = networkType,
onNavigateToSettings = { navController.navigate("settings") }
)
}
composable("settings") {
SettingsScreen(
viewModel = settingsViewModel,
onNavigateBack = { navController.popBackStack() },
onRequestVpnPermission = { requestVpnPermission() }
)
}
}

composable("settings") {
SettingsScreen(
viewModel = settingsViewModel,
onNavigateBack = { navController.popBackStack() },
onRequestVpnPermission = { requestVpnPermission() }
)
}
}

// If onboarding was just completed, navigate to main
if (onboardingCompleted && navController.currentDestination?.route == "onboarding") {
navController.navigate("main") {
popUpTo("onboarding") { inclusive = true }
if (onboardingCompleted && navController.currentDestination?.route == "onboarding") {
navController.navigate("main") {
popUpTo("onboarding") { inclusive = true }
}
}
}
}
Expand All @@ -124,9 +126,8 @@ class MainActivity : ComponentActivity() {
if (prepareIntent != null) {
vpnPermissionLauncher.launch(prepareIntent)
} else {
// Permission already granted
settingsViewModel.startVpn()
settingsViewModel.completeOnboarding()
}
}
}
}
48 changes: 48 additions & 0 deletions app/src/main/java/com/yogesh/appfence/ui/screens/SplashScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.yogesh.appfence.ui.screens

import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.yogesh.appfence.R

@Composable
fun AppFenceSplashScreen() {
val infiniteTransition = rememberInfiniteTransition(label = "SplashRotation")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "LogoAngle"
)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF001A35)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.circle_logo),
contentDescription = "Loading...",
modifier = Modifier
.size(192.dp)
.rotate(rotationAngle)
)
}
}
Binary file added app/src/main/res/drawable/circle_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
5 changes: 5 additions & 0 deletions app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file removed app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary file not shown.
Binary file added app/src/main/res/mipmap-hdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary file not shown.
Binary file added app/src/main/res/mipmap-mdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary file not shown.
Binary file added app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary file not shown.
Binary file added app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary file not shown.
Binary file removed app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Diff not rendered.
Binary file added app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Diff not rendered.
Diff not rendered.
5 changes: 5 additions & 0 deletions app/src/main/res/values/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="splash_background">#0D1B2A</color>
<color name="splash_teal">#00E5CC</color>
</resources>
4 changes: 2 additions & 2 deletions app/src/main/res/values/ic_launcher_background.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0D1B2A</color>
</resources>
<color name="ic_launcher_background">#041D3B</color>
</resources>
11 changes: 9 additions & 2 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AppFence" parent="android:Theme.Material.NoActionBar" />
</resources>
<style name="Theme.AppFence" parent="android:Theme.NoTitleBar">
</style>

<style name="Theme.AppFence.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">@drawable/circle_logo</item>
<item name="windowSplashScreenBackground">#001A35</item>
<item name="postSplashScreenTheme">@style/Theme.AppFence</item>
</style>
</resources>
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ navigationCompose = "2.8.5"
room = "2.6.1"
coroutines = "1.9.0"
datastorePreferences = "1.1.1"
splashscreen = "1.0.1"
interpolator = "1.0.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand All @@ -30,6 +32,8 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
androidx-interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down