diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
index 2eaf9be63..2b1e9fc4f 100644
--- a/core/src/main/AndroidManifest.xml
+++ b/core/src/main/AndroidManifest.xml
@@ -61,21 +61,68 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
.
+*/
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import net.ivpn.core.R
+import net.ivpn.core.common.dagger.ApplicationScope
+import javax.inject.Inject
+
+private const val ACTIVITY_ALIAS_PREFIX = "net.ivpn.client"
+
+@ApplicationScope
+class AppIconManager @Inject constructor(
+ private val context: Context
+) {
+
+ private var currentIcon: CustomAppIconData? = null
+
+ fun setNewAppIcon(desiredAppIcon: CustomAppIconData) {
+ val currentIconData = getCurrentIconData()
+
+ // Disable current icon
+ context.packageManager.setComponentEnabledSetting(
+ currentIconData.getComponentName(context),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP
+ )
+
+ // Enable new icon
+ context.packageManager.setComponentEnabledSetting(
+ desiredAppIcon.getComponentName(context),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ )
+
+ currentIcon = desiredAppIcon
+ }
+
+ fun getCurrentIconData(): CustomAppIconData {
+ currentIcon?.let { return it }
+
+ val activeIcon = CustomAppIconData.entries.firstOrNull {
+ context.packageManager.getComponentEnabledSetting(it.getComponentName(context)) ==
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ }
+
+ currentIcon = activeIcon ?: CustomAppIconData.DEFAULT
+ return currentIcon!!
+ }
+}
+
+enum class CustomAppIconData(
+ private val componentName: String,
+ @DrawableRes val iconPreviewResId: Int,
+ @StringRes val labelResId: Int,
+ val category: IconCategory
+) {
+ DEFAULT(".MainActivity", R.mipmap.ic_launcher, R.string.app_icon_name_default, IconCategory.IVPN),
+ WEATHER(".MainActivityWeather", R.mipmap.ic_launcher_weather, R.string.app_icon_name_weather, IconCategory.Discreet),
+ NOTES(".MainActivityNotes", R.mipmap.ic_launcher_notes, R.string.app_icon_name_notes, IconCategory.Discreet),
+ CALCULATOR(".MainActivityCalculator", R.mipmap.ic_launcher_calculator, R.string.app_icon_name_calculator, IconCategory.Discreet);
+
+ fun getComponentName(context: Context): ComponentName {
+ val applicationContext = context.applicationContext
+ return ComponentName(applicationContext, ACTIVITY_ALIAS_PREFIX + componentName)
+ }
+
+ enum class IconCategory {
+ IVPN,
+ Discreet
+ }
+}
+
diff --git a/core/src/main/java/net/ivpn/core/common/dagger/ActivityComponent.java b/core/src/main/java/net/ivpn/core/common/dagger/ActivityComponent.java
index 47c73966b..bd902e513 100644
--- a/core/src/main/java/net/ivpn/core/common/dagger/ActivityComponent.java
+++ b/core/src/main/java/net/ivpn/core/common/dagger/ActivityComponent.java
@@ -29,6 +29,7 @@
import net.ivpn.core.v2.account.AccountFragment;
import net.ivpn.core.v2.account.LogOutFragment;
import net.ivpn.core.v2.alwaysonvpn.AlwaysOnVPNFragment;
+import net.ivpn.core.v2.appicon.AppIconFragment;
import net.ivpn.core.v2.antitracker.AntiTrackerFragment;
import net.ivpn.core.v2.antitracker.AntiTrackerListFragment;
import net.ivpn.core.v2.captcha.CaptchaFragment;
@@ -156,6 +157,8 @@ interface Factory {
void inject(KillSwitchFragment fragment);
+ void inject(AppIconFragment fragment);
+
void inject(MockLocationFragment fragment);
void inject(MockLocationStep1Fragment fragment);
diff --git a/core/src/main/java/net/ivpn/core/common/nightmode/NightMode.kt b/core/src/main/java/net/ivpn/core/common/nightmode/NightMode.kt
index 06875bbc7..c5cca41a3 100644
--- a/core/src/main/java/net/ivpn/core/common/nightmode/NightMode.kt
+++ b/core/src/main/java/net/ivpn/core/common/nightmode/NightMode.kt
@@ -28,17 +28,20 @@ import net.ivpn.core.R
enum class NightMode(val id: Int, val systemId: Int, val stringId: Int) {
LIGHT(R.id.light_mode, AppCompatDelegate.MODE_NIGHT_NO, R.string.settings_color_theme_light),
DARK(R.id.dark_mode, AppCompatDelegate.MODE_NIGHT_YES, R.string.settings_color_theme_dark),
+ AMOLED_BLACK(R.id.amoled_black_mode, AppCompatDelegate.MODE_NIGHT_YES, R.string.settings_color_theme_amoled_black),
SYSTEM_DEFAULT(R.id.system_default_mode, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, R.string.settings_color_theme_system_default),
BY_BATTERY_SAVER(R.id.set_by_battery_mode, AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY, R.string.settings_color_theme_system_by_battery);
+ val isOledBlack: Boolean
+ get() = this == AMOLED_BLACK
+
companion object {
- fun getById(id: Int) : NightMode {
- for (mode in values()) {
+ fun getById(id: Int): NightMode {
+ for (mode in entries) {
if (mode.id == id) {
return mode
}
}
-
return LIGHT
}
}
diff --git a/core/src/main/java/net/ivpn/core/common/nightmode/OledModeController.kt b/core/src/main/java/net/ivpn/core/common/nightmode/OledModeController.kt
new file mode 100644
index 000000000..cc355797f
--- /dev/null
+++ b/core/src/main/java/net/ivpn/core/common/nightmode/OledModeController.kt
@@ -0,0 +1,172 @@
+package net.ivpn.core.common.nightmode
+
+/*
+ IVPN Android app
+ https://github.com/ivpn/android-app
+
+ Created by Tamim Hossain.
+ Copyright (c) 2025 IVPN Limited.
+
+ This file is part of the IVPN Android app.
+
+ The IVPN Android app is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ The IVPN Android app is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
+
+ You should have received a copy of the GNU General Public License
+ along with the IVPN Android app. If not, see .
+*/
+
+import android.app.Activity
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import androidx.appcompat.widget.Toolbar
+import androidx.cardview.widget.CardView
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.tabs.TabLayout
+import net.ivpn.core.IVPNApplication
+import net.ivpn.core.R
+
+object OledModeController {
+
+ const val OLED_BLACK = Color.BLACK
+ const val OLED_CARD = 0xFF0A0A0A.toInt()
+ const val OLED_HANDLE = 0xFF666666.toInt()
+
+ private val darkGrayColors = setOf(
+ 0xFF202020.toInt(),
+ 0xFF1C1C1C.toInt(),
+ 0xFF1C1C1E.toInt(),
+ 0xFF121212.toInt(),
+ 0xFF323232.toInt(),
+ 0xFF383838.toInt(),
+ 0xFF292929.toInt(),
+ 0xFF181818.toInt(),
+ 0xFF343332.toInt(),
+ 0xFF060606.toInt()
+ )
+
+ private val handleColor = 0xFF49494B.toInt()
+
+ @JvmStatic
+ fun applyOledTheme(activity: Activity) {
+ if (isOledModeEnabled()) {
+ activity.setTheme(R.style.AppTheme_OLED)
+ }
+ }
+
+ fun applyOledColors(window: Window, rootView: View?) {
+ if (!isOledModeEnabled()) return
+
+ window.statusBarColor = OLED_BLACK
+ window.navigationBarColor = OLED_BLACK
+ window.decorView.setBackgroundColor(OLED_BLACK)
+ rootView?.let { applyOledToViewTree(it) }
+ }
+
+ fun applyOledToViewTree(view: View) {
+ if (!isOledModeEnabled()) return
+
+ val background = view.background?.mutate()
+ when (background) {
+ is ColorDrawable -> {
+ if (background.color in darkGrayColors) {
+ view.setBackgroundColor(OLED_BLACK)
+ }
+ }
+ is GradientDrawable -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ background.color?.defaultColor?.let { gradientColor ->
+ when (gradientColor) {
+ handleColor -> background.setColor(OLED_HANDLE)
+ in darkGrayColors -> background.setColor(OLED_BLACK)
+ }
+ }
+ }
+ }
+ is LayerDrawable -> {
+ for (i in 0 until background.numberOfLayers) {
+ val layer = background.getDrawable(i)?.mutate()
+ if (layer is GradientDrawable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ layer.color?.defaultColor?.let { layerColor ->
+ when (layerColor) {
+ handleColor -> layer.setColor(OLED_HANDLE)
+ in darkGrayColors -> layer.setColor(OLED_BLACK)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (view is CardView) {
+ val cardColor = view.cardBackgroundColor.defaultColor
+ if (cardColor in darkGrayColors) {
+ view.setCardBackgroundColor(OLED_CARD)
+ }
+ }
+
+ if (view is FloatingActionButton) {
+ val fabColor = view.backgroundTintList?.defaultColor ?: 0
+ if (fabColor in darkGrayColors) {
+ view.backgroundTintList = ColorStateList.valueOf(OLED_CARD)
+ }
+ }
+
+ if (view is Toolbar) {
+ view.setBackgroundColor(OLED_BLACK)
+ }
+
+ if (view is AppBarLayout) {
+ view.setBackgroundColor(OLED_BLACK)
+ }
+
+ if (view is TabLayout) {
+ view.setBackgroundColor(OLED_BLACK)
+ }
+
+ view.backgroundTintList?.let { tintList ->
+ val tintColor = tintList.defaultColor
+ if (tintColor in darkGrayColors) {
+ view.backgroundTintList = ColorStateList.valueOf(OLED_CARD)
+ }
+ }
+
+ if (view is ViewGroup) {
+ for (i in 0 until view.childCount) {
+ applyOledToViewTree(view.getChildAt(i))
+ }
+ }
+ }
+
+ fun getBackgroundColor(): Int {
+ return if (isOledModeEnabled()) OLED_BLACK else 0
+ }
+
+ fun getCardColor(): Int {
+ return if (isOledModeEnabled()) OLED_CARD else 0
+ }
+
+ fun isOledModeEnabled(): Boolean {
+ return try {
+ val settings = IVPNApplication.appComponent.provideSettings()
+ settings?.nightMode?.isOledBlack == true
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
+
diff --git a/core/src/main/java/net/ivpn/core/common/prefs/EncryptedSettingsPreference.kt b/core/src/main/java/net/ivpn/core/common/prefs/EncryptedSettingsPreference.kt
index 6ff545711..6c7c982d8 100644
--- a/core/src/main/java/net/ivpn/core/common/prefs/EncryptedSettingsPreference.kt
+++ b/core/src/main/java/net/ivpn/core/common/prefs/EncryptedSettingsPreference.kt
@@ -79,6 +79,7 @@ class EncryptedSettingsPreference @Inject constructor(val preference: Preference
private const val LAST_USED_IP = "LAST_USED_IP"
private const val ANTITRACKER_LIST = "ANTITRACKER_LIST"
private const val ANTITRACKER_DNS = "ANTITRACKER_DNS"
+ private const val WIREGUARD_MTU = "WIREGUARD_MTU"
}
private val sharedPreferences: SharedPreferences = preference.settingsPreference
@@ -517,6 +518,15 @@ class EncryptedSettingsPreference @Inject constructor(val preference: Preference
return sharedPreferences.getString(ANTITRACKER_DNS, "")
}
+ var wireGuardMtu: Int
+ get() {
+ return sharedPreferences.getInt(WIREGUARD_MTU, 0)
+ }
+ set(value) {
+ sharedPreferences.edit {
+ putInt(WIREGUARD_MTU, value)
+ }
+ }
private fun putIsMigrated(isMigrated: Boolean) {
sharedPreferences.edit {
diff --git a/core/src/main/java/net/ivpn/core/common/prefs/Settings.kt b/core/src/main/java/net/ivpn/core/common/prefs/Settings.kt
index c60d27244..4b4997126 100644
--- a/core/src/main/java/net/ivpn/core/common/prefs/Settings.kt
+++ b/core/src/main/java/net/ivpn/core/common/prefs/Settings.kt
@@ -366,6 +366,12 @@ class Settings @Inject constructor(
settingsPreference.putRegenerationPeriod(value)
}
+ var wireGuardMtu: Int
+ get() = settingsPreference.wireGuardMtu
+ set(value) {
+ settingsPreference.wireGuardMtu = value
+ }
+
fun generateWireGuardKeys(): Keypair {
return Keypair()
}
diff --git a/core/src/main/java/net/ivpn/core/common/shortcuts/ConnectionShortcutsActivity.java b/core/src/main/java/net/ivpn/core/common/shortcuts/ConnectionShortcutsActivity.java
index 16505ec5e..bd4e0d141 100644
--- a/core/src/main/java/net/ivpn/core/common/shortcuts/ConnectionShortcutsActivity.java
+++ b/core/src/main/java/net/ivpn/core/common/shortcuts/ConnectionShortcutsActivity.java
@@ -33,6 +33,7 @@
import androidx.appcompat.app.AppCompatActivity;
import net.ivpn.core.IVPNApplication;
+import net.ivpn.core.common.nightmode.OledModeController;
import net.ivpn.core.v2.dialog.DialogBuilder;
import net.ivpn.core.v2.dialog.Dialogs;
@@ -54,6 +55,7 @@ public class ConnectionShortcutsActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ OledModeController.applyOledTheme(this);
LOGGER.info("ConnectionShortcutsActivity onCreate");
IVPNApplication.appComponent.provideActivityComponent().create().inject(this);
super.onCreate(savedInstanceState);
diff --git a/core/src/main/java/net/ivpn/core/ui/theme/Color.kt b/core/src/main/java/net/ivpn/core/ui/theme/Color.kt
index 76300728b..97d474394 100644
--- a/core/src/main/java/net/ivpn/core/ui/theme/Color.kt
+++ b/core/src/main/java/net/ivpn/core/ui/theme/Color.kt
@@ -5,6 +5,9 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
val colorPrimary = Color(0xFF398FE6)
+val oledBackground = Color(0xFF000000)
+val oledSurface = Color(0xFF000000)
+val oledCard = Color(0xFF0A0A0A)
@Immutable
data class CustomColors(
@@ -12,7 +15,9 @@ data class CustomColors(
val textFieldBackground: Color = Color.Unspecified,
val textFieldText: Color = Color.Unspecified,
val textFieldPlaceholder: Color = Color.Unspecified,
- val secondaryLabel: Color = Color.Unspecified
+ val secondaryLabel: Color = Color.Unspecified,
+ val background: Color = Color.Unspecified,
+ val surface: Color = Color.Unspecified
)
val LocalColors = staticCompositionLocalOf {
@@ -24,7 +29,9 @@ val CustomLightColorPalette = CustomColors(
textFieldBackground = Color(0x54D3DFE6),
textFieldText = Color(0xFF2A394B),
textFieldPlaceholder = Color(0x802A394B),
- secondaryLabel = Color(0x80000000)
+ secondaryLabel = Color(0x80000000),
+ background = Color(0xFFF5F9FC),
+ surface = Color(0xFFFFFFFF)
)
val CustomDarkColorPalette = CustomColors(
@@ -32,5 +39,17 @@ val CustomDarkColorPalette = CustomColors(
textFieldBackground = Color(0xFF1C1C1E),
textFieldText = Color(0xFFFFFFFF),
textFieldPlaceholder = Color(0x80FFFFFF),
- secondaryLabel = Color(0x80FFFFFF)
+ secondaryLabel = Color(0x80FFFFFF),
+ background = Color(0xFF121212),
+ surface = Color(0xFF202020)
+)
+
+val CustomOledColorPalette = CustomColors(
+ textFieldLabel = Color(0xFFFFFFFF),
+ textFieldBackground = Color(0xFF0A0A0A),
+ textFieldText = Color(0xFFFFFFFF),
+ textFieldPlaceholder = Color(0x80FFFFFF),
+ secondaryLabel = Color(0x80FFFFFF),
+ background = oledBackground,
+ surface = oledSurface
)
diff --git a/core/src/main/java/net/ivpn/core/ui/theme/Theme.kt b/core/src/main/java/net/ivpn/core/ui/theme/Theme.kt
index 252da2fc5..55b70a1d1 100644
--- a/core/src/main/java/net/ivpn/core/ui/theme/Theme.kt
+++ b/core/src/main/java/net/ivpn/core/ui/theme/Theme.kt
@@ -6,26 +6,42 @@ import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import net.ivpn.core.common.nightmode.OledModeController
private val DarkColorPalette = darkColors(
primary = colorPrimary,
secondary = colorPrimary
)
+private val OledColorPalette = darkColors(
+ primary = colorPrimary,
+ secondary = colorPrimary,
+ background = oledBackground,
+ surface = oledSurface
+)
+
private val LightColorPalette = lightColors(
primary = colorPrimary,
secondary = colorPrimary
)
@Composable
-fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
- val colors =
- if (darkTheme) DarkColorPalette
- else LightColorPalette
+fun AppTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ oledTheme: Boolean = OledModeController.isOledModeEnabled(),
+ content: @Composable() () -> Unit
+) {
+ val colors = when {
+ oledTheme -> OledColorPalette
+ darkTheme -> DarkColorPalette
+ else -> LightColorPalette
+ }
- val customColors =
- if (darkTheme) CustomDarkColorPalette
- else CustomLightColorPalette
+ val customColors = when {
+ oledTheme -> CustomOledColorPalette
+ darkTheme -> CustomDarkColorPalette
+ else -> CustomLightColorPalette
+ }
CompositionLocalProvider(
LocalColors provides customColors
diff --git a/core/src/main/java/net/ivpn/core/v2/MainActivity.kt b/core/src/main/java/net/ivpn/core/v2/MainActivity.kt
index ebe021966..ceb757aef 100644
--- a/core/src/main/java/net/ivpn/core/v2/MainActivity.kt
+++ b/core/src/main/java/net/ivpn/core/v2/MainActivity.kt
@@ -34,12 +34,15 @@ import androidx.navigation.findNavController
import net.ivpn.core.IVPNApplication
import net.ivpn.core.R
import net.ivpn.core.common.extension.setContentSecure
+import net.ivpn.core.common.nightmode.OledModeController
class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener {
override fun onCreate(savedInstanceState: Bundle?) {
+ OledModeController.applyOledTheme(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+ OledModeController.applyOledColors(window, findViewById(android.R.id.content))
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -57,6 +60,11 @@ class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedList
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
currentFocus?.hideKeyboard()
+ findViewById(R.id.nav_host_fragment)?.post {
+ findViewById(R.id.nav_host_fragment)?.let {
+ OledModeController.applyOledToViewTree(it)
+ }
+ }
}
private fun View.hideKeyboard() {
diff --git a/core/src/main/java/net/ivpn/core/v2/account/LogOutFragment.kt b/core/src/main/java/net/ivpn/core/v2/account/LogOutFragment.kt
index efe013014..760f21a3f 100644
--- a/core/src/main/java/net/ivpn/core/v2/account/LogOutFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/account/LogOutFragment.kt
@@ -33,6 +33,7 @@ import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.ivpn.core.IVPNApplication
import net.ivpn.core.R
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.databinding.FragmentLogoutBottomSheetBinding
import net.ivpn.core.v2.viewmodel.AccountViewModel
import javax.inject.Inject
@@ -44,6 +45,14 @@ class LogOutFragment : BottomSheetDialogFragment() {
@Inject
lateinit var account: AccountViewModel
+ override fun getTheme(): Int {
+ return if (OledModeController.isOledModeEnabled()) {
+ R.style.AppTheme_BottomSheet_OLED
+ } else {
+ super.getTheme()
+ }
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -58,6 +67,9 @@ class LogOutFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
+ if (OledModeController.isOledModeEnabled()) {
+ binding.bottomSheet.setBackgroundColor(requireContext().getColor(R.color.oled_background))
+ }
init()
}
diff --git a/core/src/main/java/net/ivpn/core/v2/appicon/AppIconFragment.kt b/core/src/main/java/net/ivpn/core/v2/appicon/AppIconFragment.kt
new file mode 100644
index 000000000..9ea1d6d4f
--- /dev/null
+++ b/core/src/main/java/net/ivpn/core/v2/appicon/AppIconFragment.kt
@@ -0,0 +1,140 @@
+package net.ivpn.core.v2.appicon
+
+/*
+ IVPN Android app
+ https://github.com/ivpn/android-app
+
+ Created by Tamim Hossain.
+ Copyright (c) 2025 IVPN Limited.
+
+ This file is part of the IVPN Android app.
+
+ The IVPN Android app is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ The IVPN Android app is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
+
+ You should have received a copy of the GNU General Public License
+ along with the IVPN Android app. If not, see .
+*/
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupWithNavController
+import net.ivpn.core.IVPNApplication
+import net.ivpn.core.R
+import net.ivpn.core.common.appicon.CustomAppIconData
+import net.ivpn.core.common.nightmode.OledModeController
+import net.ivpn.core.databinding.FragmentAppIconBinding
+import net.ivpn.core.v2.MainActivity
+import net.ivpn.core.v2.viewmodel.AppIconViewModel
+import javax.inject.Inject
+
+class AppIconFragment : Fragment() {
+
+ private lateinit var binding: FragmentAppIconBinding
+
+ @Inject
+ lateinit var viewModel: AppIconViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = DataBindingUtil.inflate(inflater, R.layout.fragment_app_icon, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
+ initToolbar()
+ initViews()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ viewModel.onResume()
+ updateRadioButtons()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ activity?.let {
+ if (it is MainActivity) {
+ it.setContentSecure(false)
+ }
+ }
+ }
+
+ private fun initToolbar() {
+ val navController = findNavController()
+ val appBarConfiguration = AppBarConfiguration(navController.graph)
+ binding.toolbar.setupWithNavController(navController, appBarConfiguration)
+ }
+
+ private fun initViews() {
+ binding.contentLayout.viewModel = viewModel
+
+ binding.contentLayout.iconDefault.setOnClickListener {
+ showConfirmationDialog(CustomAppIconData.DEFAULT)
+ }
+
+ binding.contentLayout.iconWeather.setOnClickListener {
+ showConfirmationDialog(CustomAppIconData.WEATHER)
+ }
+
+ binding.contentLayout.iconNotes.setOnClickListener {
+ showConfirmationDialog(CustomAppIconData.NOTES)
+ }
+
+ binding.contentLayout.iconCalculator.setOnClickListener {
+ showConfirmationDialog(CustomAppIconData.CALCULATOR)
+ }
+ }
+
+ private fun showConfirmationDialog(icon: CustomAppIconData) {
+ if (viewModel.isSelected(icon)) {
+ return
+ }
+
+ AlertDialog.Builder(requireContext(), getDialogStyle())
+ .setTitle(R.string.app_icon_change_title)
+ .setMessage(R.string.app_icon_change_message)
+ .setPositiveButton(R.string.app_icon_change_confirm) { _, _ ->
+ viewModel.selectIcon(icon)
+ updateRadioButtons()
+ }
+ .setNegativeButton(R.string.dialog_cancel, null)
+ .show()
+ }
+
+ private fun updateRadioButtons() {
+ val currentIcon = viewModel.currentIcon.get()
+ binding.contentLayout.radioDefault.isChecked = currentIcon == CustomAppIconData.DEFAULT
+ binding.contentLayout.radioWeather.isChecked = currentIcon == CustomAppIconData.WEATHER
+ binding.contentLayout.radioNotes.isChecked = currentIcon == CustomAppIconData.NOTES
+ binding.contentLayout.radioCalculator.isChecked = currentIcon == CustomAppIconData.CALCULATOR
+ }
+
+ private fun getDialogStyle(): Int {
+ return if (OledModeController.isOledModeEnabled()) {
+ R.style.AppTheme_AlertDialog_OLED
+ } else {
+ R.style.AppTheme_AlertDialog
+ }
+ }
+}
+
diff --git a/core/src/main/java/net/ivpn/core/v2/connect/ConnectFragment.kt b/core/src/main/java/net/ivpn/core/v2/connect/ConnectFragment.kt
index d717d44c9..90781a826 100644
--- a/core/src/main/java/net/ivpn/core/v2/connect/ConnectFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/connect/ConnectFragment.kt
@@ -71,6 +71,7 @@ import net.ivpn.core.v2.map.model.Location
import net.ivpn.core.v2.network.NetworkViewModel
import net.ivpn.core.v2.signup.SignUpController
import net.ivpn.core.v2.viewmodel.*
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.vpn.ServiceConstants
import org.slf4j.LoggerFactory
import javax.inject.Inject
@@ -143,6 +144,7 @@ class ConnectFragment : Fragment(), MultiHopViewModel.MultiHopNavigator,
LOGGER.info("On view created")
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
initViews()
+ view.post { OledModeController.applyOledToViewTree(view) }
// Support variable bottom navigation height (Gesture, 2-Button, 3-Button) for Android 35+
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
@@ -176,6 +178,7 @@ class ConnectFragment : Fragment(), MultiHopViewModel.MultiHopNavigator,
account.updateSessionStatus()
checkLocationPermission()
applySlidingPanelSide()
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onStart() {
diff --git a/core/src/main/java/net/ivpn/core/v2/connect/createSession/CreateSessionFragment.java b/core/src/main/java/net/ivpn/core/v2/connect/createSession/CreateSessionFragment.java
index d1b452a78..7a54b5b82 100644
--- a/core/src/main/java/net/ivpn/core/v2/connect/createSession/CreateSessionFragment.java
+++ b/core/src/main/java/net/ivpn/core/v2/connect/createSession/CreateSessionFragment.java
@@ -38,6 +38,7 @@
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import net.ivpn.core.R;
+import net.ivpn.core.common.nightmode.OledModeController;
import net.ivpn.core.databinding.BottomSheetBinding;
import net.ivpn.core.databinding.BottomSheetDmProBinding;
import net.ivpn.core.databinding.BottomSheetLegacyStandardBinding;
@@ -60,6 +61,14 @@ public CreateSessionFragment(SessionErrorResponse error) {
this.error = error;
}
+ @Override
+ public int getTheme() {
+ if (OledModeController.INSTANCE.isOledModeEnabled()) {
+ return R.style.AppTheme_BottomSheet_OLED;
+ }
+ return super.getTheme();
+ }
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -112,6 +121,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
+ if (OledModeController.INSTANCE.isOledModeEnabled()) {
+ view.setBackgroundColor(requireContext().getColor(R.color.oled_background));
+ }
init();
}
diff --git a/core/src/main/java/net/ivpn/core/v2/customdns/CustomDNSFragment.kt b/core/src/main/java/net/ivpn/core/v2/customdns/CustomDNSFragment.kt
index 149bd115c..76b41e3f1 100644
--- a/core/src/main/java/net/ivpn/core/v2/customdns/CustomDNSFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/customdns/CustomDNSFragment.kt
@@ -26,6 +26,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.ViewTreeObserver
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
@@ -34,6 +35,7 @@ import androidx.navigation.ui.setupWithNavController
import net.ivpn.core.IVPNApplication
import net.ivpn.core.R
import net.ivpn.core.databinding.FragmentCustomDnsBinding
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.v2.dialog.DialogBuilder
import net.ivpn.core.v2.MainActivity
import org.slf4j.LoggerFactory
@@ -50,6 +52,8 @@ class CustomDNSFragment : Fragment() {
@Inject
lateinit var viewModel: CustomDNSViewModel
+ private var oledLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -64,6 +68,19 @@ class CustomDNSFragment : Fragment() {
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
initViews()
initToolbar()
+ view.post { OledModeController.applyOledToViewTree(view) }
+ registerOledEnforcer(view)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ applyOledIfNeeded()
+ binding.root.post { applyOledIfNeeded() }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ unregisterOledEnforcer()
}
override fun onStart() {
@@ -83,6 +100,35 @@ class CustomDNSFragment : Fragment() {
}
}
+ private fun applyOledIfNeeded() {
+ if (!OledModeController.isOledModeEnabled()) return
+ if (!isAdded) return
+ val context = context ?: return
+ val oledColor = context.getColor(R.color.oled_background)
+ binding.root.setBackgroundColor(oledColor)
+ binding.coordinator.setBackgroundColor(oledColor)
+ binding.contentLayout.root.setBackgroundColor(oledColor)
+ OledModeController.applyOledToViewTree(binding.root)
+ }
+
+ private fun registerOledEnforcer(view: View) {
+ if (!OledModeController.isOledModeEnabled()) return
+ if (oledLayoutListener != null) return
+ if (!view.viewTreeObserver.isAlive) return
+ oledLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { applyOledIfNeeded() }
+ view.viewTreeObserver.addOnGlobalLayoutListener(oledLayoutListener)
+ }
+
+ private fun unregisterOledEnforcer() {
+ oledLayoutListener?.let { listener ->
+ val v = view
+ if (v != null && v.viewTreeObserver.isAlive) {
+ v.viewTreeObserver.removeOnGlobalLayoutListener(listener)
+ }
+ }
+ oledLayoutListener = null
+ }
+
fun changeDNS() {
DialogBuilder.createCustomDNSDialogue(context) { dns: String? -> viewModel.setDnsAs(dns) }
}
diff --git a/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilder.kt b/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilder.kt
index 656315f0a..15c4f3c10 100644
--- a/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilder.kt
+++ b/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilder.kt
@@ -47,17 +47,27 @@ import net.ivpn.core.common.InputFilterMinMax
import net.ivpn.core.common.extension.setContentSecure
import net.ivpn.core.common.utils.DateUtil
import net.ivpn.core.databinding.DialogCustomDnsBinding
+import net.ivpn.core.databinding.DialogMtuBinding
import net.ivpn.core.v2.customdns.OnDNSChangedListener
import net.ivpn.core.v2.protocol.dialog.WireGuardDetailsDialogListener
import net.ivpn.core.v2.protocol.dialog.WireGuardInfo
import net.ivpn.core.v2.timepicker.OnDelayOptionSelected
import net.ivpn.core.v2.timepicker.PauseDelay
+import net.ivpn.core.common.nightmode.OledModeController
import org.slf4j.LoggerFactory
import java.util.*
object DialogBuilder {
private val LOGGER = LoggerFactory.getLogger(DialogBuilder::class.java)
+ private fun getDialogStyle(): Int {
+ return if (OledModeController.isOledModeEnabled()) {
+ R.style.AppTheme_AlertDialog_OLED
+ } else {
+ R.style.AlertDialog
+ }
+ }
+
@JvmStatic
fun createOptionDialog(
context: Context?, dialogAttr: Dialogs,
@@ -67,7 +77,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = MaterialAlertDialogBuilder(context, R.style.AlertDialog)
+ val builder = MaterialAlertDialogBuilder(context, getDialogStyle())
builder.setTitle(context.getString(dialogAttr.titleId))
builder.setMessage(context.getString(dialogAttr.messageId))
if (dialogAttr.positiveBtnId != -1) {
@@ -94,7 +104,7 @@ object DialogBuilder {
if (context == null || dialogAttr == null) {
return
}
- val builder = MaterialAlertDialogBuilder(context, R.style.AlertDialog)
+ val builder = MaterialAlertDialogBuilder(context, getDialogStyle())
builder.setTitle(context.getString(dialogAttr.titleId))
builder.setMessage(context.getString(dialogAttr.messageId))
builder.setNegativeButton(context.getString(dialogAttr.negativeBtnId), null)
@@ -116,7 +126,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = MaterialAlertDialogBuilder(context, R.style.AlertDialog)
+ val builder = MaterialAlertDialogBuilder(context, getDialogStyle())
builder.setTitle(title)
builder.setMessage(msg)
builder.setNegativeButton(context.getString(R.string.dialogs_ok), null)
@@ -141,7 +151,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
builder.setTitle(title)
builder.setMessage(msg)
builder.setOnCancelListener(cancelListener)
@@ -173,7 +183,7 @@ object DialogBuilder {
if (context == null) {
return null
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
builder.setTitle(context.getString(dialogAttr.titleId))
builder.setMessage(context.getString(dialogAttr.messageId))
@@ -215,7 +225,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val dialogView = inflater.inflate(R.layout.dialog_predefined_time_picker, null)
val delayMap: MutableMap = HashMap()
@@ -258,7 +268,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val dialogView = inflater.inflate(R.layout.dialog_custom_time_picker, null)
val pauseTime = LongArray(1)
@@ -299,9 +309,12 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val dialogView = inflater.inflate(R.layout.dialog_wireguard_details, null)
+ if (OledModeController.isOledModeEnabled()) {
+ dialogView.setBackgroundColor(context.getColor(R.color.oled_background))
+ }
builder.setView(dialogView)
val alertDialog = builder.create()
(dialogView.findViewById(R.id.wg_public_key) as TextView).text = info.publicKey
@@ -339,7 +352,7 @@ object DialogBuilder {
if (context == null) {
return
}
- val builder = AlertDialog.Builder(context, R.style.AlertDialog)
+ val builder = AlertDialog.Builder(context, getDialogStyle())
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val viewModel =
IVPNApplication.appComponent.provideActivityComponent().create().dialogueViewModel
@@ -350,6 +363,9 @@ object DialogBuilder {
)
binding.viewmodel = viewModel
val dialogView = binding.root
+ if (OledModeController.isOledModeEnabled()) {
+ dialogView.setBackgroundColor(context.getColor(R.color.oled_background))
+ }
builder.setView(dialogView)
val alertDialog = builder.create()
binding.firstValue.filters = arrayOf(InputFilterMinMax(0, 255))
@@ -379,4 +395,76 @@ object DialogBuilder {
exception.printStackTrace()
}
}
+
+ @JvmStatic
+ fun createMtuDialog(
+ context: Context?,
+ currentMtu: String,
+ onMtuSaved: (String) -> Unit,
+ onMtuError: () -> Unit
+ ) {
+ LOGGER.info("Create MTU dialog")
+ if (context == null) {
+ return
+ }
+ val builder = AlertDialog.Builder(context, getDialogStyle())
+ val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ val binding: DialogMtuBinding = DataBindingUtil.inflate(
+ inflater,
+ R.layout.dialog_mtu, null, false
+ )
+ binding.mtuValue = currentMtu
+ val dialogView = binding.root
+ if (OledModeController.isOledModeEnabled()) {
+ dialogView.setBackgroundColor(context.getColor(R.color.oled_background))
+ binding.mtuInput.setHintTextColor(context.getColor(R.color.oled_hint_text))
+ }
+ builder.setView(dialogView)
+ val alertDialog = builder.create()
+
+ binding.mtuInput.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ val mtuString = binding.mtuInput.text?.toString() ?: ""
+ if (isValidMtu(mtuString)) {
+ onMtuSaved(mtuString)
+ alertDialog.dismiss()
+ } else {
+ onMtuError()
+ }
+ }
+ false
+ }
+
+ dialogView.findViewById(R.id.apply_action).setOnClickListener {
+ val mtuString = binding.mtuInput.text?.toString() ?: ""
+ if (isValidMtu(mtuString)) {
+ onMtuSaved(mtuString)
+ alertDialog.dismiss()
+ } else {
+ onMtuError()
+ }
+ }
+ dialogView.findViewById(R.id.cancel_action).setOnClickListener { alertDialog.dismiss() }
+
+ if ((context as Activity).isFinishing) {
+ return
+ }
+ try {
+ alertDialog.show()
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+
+ private fun isValidMtu(mtuString: String): Boolean {
+ if (mtuString.isEmpty()) {
+ return true // Empty means use default
+ }
+ return try {
+ val mtu = mtuString.toInt()
+ mtu == 0 || (mtu in 576..65535)
+ } catch (e: NumberFormatException) {
+ false
+ }
+ }
}
\ No newline at end of file
diff --git a/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilderK.kt b/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilderK.kt
index ec6bbb6e7..9b29dda56 100644
--- a/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilderK.kt
+++ b/core/src/main/java/net/ivpn/core/v2/dialog/DialogBuilderK.kt
@@ -36,12 +36,21 @@ import net.ivpn.core.databinding.DialogueNightModeBinding
import net.ivpn.core.v2.network.dialog.NetworkChangeDialogViewModel
import net.ivpn.core.v2.viewmodel.ColorThemeViewModel
import net.ivpn.core.v2.viewmodel.ServerListFilterViewModel
+import net.ivpn.core.common.nightmode.OledModeController
object DialogBuilderK {
+ private fun getDialogStyle(): Int {
+ return if (OledModeController.isOledModeEnabled()) {
+ R.style.AppTheme_AlertDialog_OLED
+ } else {
+ R.style.AppTheme_AlertDialog
+ }
+ }
+
fun openDarkModeDialogue(context: Context, listener: OnNightModeChangedListener, colorThemeViewModel: ColorThemeViewModel) {
val builder: AlertDialog.Builder =
- AlertDialog.Builder(context, R.style.AppTheme_AlertDialog)
+ AlertDialog.Builder(context, getDialogStyle())
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -75,7 +84,7 @@ object DialogBuilderK {
listener: ServerListFilterViewModel.OnFilterChangedListener,
filterViewModel: ServerListFilterViewModel) {
val builder: AlertDialog.Builder =
- AlertDialog.Builder(context, R.style.AppTheme_AlertDialog)
+ AlertDialog.Builder(context, getDialogStyle())
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -104,7 +113,7 @@ object DialogBuilderK {
fun openChangeNetworkStatusDialogue(context: Context, dialogViewModel: NetworkChangeDialogViewModel) {
val builder: AlertDialog.Builder =
- AlertDialog.Builder(context, R.style.AppTheme_AlertDialog)
+ AlertDialog.Builder(context, getDialogStyle())
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -133,7 +142,7 @@ object DialogBuilderK {
fun openChangeDefaultNetworkStatusDialogue(context: Context, dialogViewModel: NetworkChangeDialogViewModel) {
val builder: AlertDialog.Builder =
- AlertDialog.Builder(context, R.style.AppTheme_AlertDialog)
+ AlertDialog.Builder(context, getDialogStyle())
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
diff --git a/core/src/main/java/net/ivpn/core/v2/network/NetworkCommonFragment.kt b/core/src/main/java/net/ivpn/core/v2/network/NetworkCommonFragment.kt
index 7e0eeafb0..0b23af4fe 100644
--- a/core/src/main/java/net/ivpn/core/v2/network/NetworkCommonFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/network/NetworkCommonFragment.kt
@@ -44,6 +44,7 @@ import androidx.navigation.ui.setupWithNavController
import net.ivpn.core.IVPNApplication
import net.ivpn.core.R
import net.ivpn.core.databinding.FragmentNetworkBinding
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.v2.dialog.DialogBuilder
import net.ivpn.core.v2.dialog.Dialogs
import net.ivpn.core.v2.MainActivity
@@ -81,6 +82,7 @@ class NetworkCommonFragment : Fragment(), NetworkNavigator {
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
initViews()
initToolbar()
+ view.post { OledModeController.applyOledToViewTree(view) }
}
@Deprecated("Deprecated in Java")
diff --git a/core/src/main/java/net/ivpn/core/v2/network/NetworkRecyclerViewAdapter.kt b/core/src/main/java/net/ivpn/core/v2/network/NetworkRecyclerViewAdapter.kt
index 9e900081b..007f18143 100644
--- a/core/src/main/java/net/ivpn/core/v2/network/NetworkRecyclerViewAdapter.kt
+++ b/core/src/main/java/net/ivpn/core/v2/network/NetworkRecyclerViewAdapter.kt
@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import net.ivpn.core.IVPNApplication
import net.ivpn.core.databinding.ViewWifiItemBinding
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.v2.dialog.DialogBuilderK.openChangeNetworkStatusDialogue
import net.ivpn.core.v2.network.dialog.NetworkChangeDialogViewModel
import net.ivpn.core.vpn.model.NetworkState
@@ -121,6 +122,7 @@ class NetworkRecyclerViewAdapter(context: Context?) : RecyclerView.Adapter
+ viewModel.saveMtu(mtuString)
+ },
+ onMtuError = {
+ DialogBuilder.createFullCustomNotificationDialog(
+ context,
+ getString(R.string.dialogs_error),
+ getString(R.string.protocol_wg_mtu_error)
+ )
+ }
+ )
+ }
+
private fun openQuantumResistanceInfo() {
DialogBuilder.createNotificationDialog(context, Dialogs.WG_QUANTUM_RESISTANCE_INFO)
}
diff --git a/core/src/main/java/net/ivpn/core/v2/serverlist/ServerListTabFragment.kt b/core/src/main/java/net/ivpn/core/v2/serverlist/ServerListTabFragment.kt
index 5e4fe0cca..e264bc301 100644
--- a/core/src/main/java/net/ivpn/core/v2/serverlist/ServerListTabFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/serverlist/ServerListTabFragment.kt
@@ -41,6 +41,7 @@ import net.ivpn.core.rest.data.model.ServerType
import net.ivpn.core.databinding.FragmentTabsServerListBinding
import net.ivpn.core.v2.viewmodel.ServersListCommonViewModel
import net.ivpn.core.v2.MainActivity
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.v2.dialog.DialogBuilderK
import net.ivpn.core.v2.serverlist.dialog.Filters
import net.ivpn.core.v2.viewmodel.ServerListFilterViewModel
@@ -79,6 +80,7 @@ class ServerListTabFragment : Fragment(), ServerListFilterViewModel.OnFilterChan
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
initViews()
initToolbar()
+ view.post { OledModeController.applyOledToViewTree(view) }
// Support variable bottom navigation height (Gesture, 2-Button, 3-Button) for Android 35+
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
@@ -97,6 +99,7 @@ class ServerListTabFragment : Fragment(), ServerListFilterViewModel.OnFilterChan
super.onResume()
LOGGER.info("onResume")
filterViewModel.onResume()
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onStart() {
diff --git a/core/src/main/java/net/ivpn/core/v2/serverlist/all/AllServersRecyclerViewAdapter.kt b/core/src/main/java/net/ivpn/core/v2/serverlist/all/AllServersRecyclerViewAdapter.kt
index 277ef6d8a..290a9372a 100644
--- a/core/src/main/java/net/ivpn/core/v2/serverlist/all/AllServersRecyclerViewAdapter.kt
+++ b/core/src/main/java/net/ivpn/core/v2/serverlist/all/AllServersRecyclerViewAdapter.kt
@@ -48,6 +48,7 @@ import net.ivpn.core.v2.serverlist.items.ConnectionOption
import net.ivpn.core.v2.serverlist.items.FastestServerItem
import net.ivpn.core.v2.serverlist.items.RandomServerItem
import net.ivpn.core.v2.serverlist.items.SearchServerItem
+import net.ivpn.core.common.nightmode.OledModeController
import org.slf4j.LoggerFactory
import java.util.*
import javax.inject.Inject
@@ -154,6 +155,8 @@ class AllServersRecyclerViewAdapter(
} else if (holder is SearchViewHolder) {
searchBinding = holder.binding
}
+ // Apply OLED colors to recycled/new items
+ OledModeController.applyOledToViewTree(holder.itemView)
}
private fun setPing(binding: ServerItemBinding, server: Server) {
diff --git a/core/src/main/java/net/ivpn/core/v2/serverlist/all/ServerListFragment.kt b/core/src/main/java/net/ivpn/core/v2/serverlist/all/ServerListFragment.kt
index f963d4874..681f681e3 100644
--- a/core/src/main/java/net/ivpn/core/v2/serverlist/all/ServerListFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/serverlist/all/ServerListFragment.kt
@@ -48,6 +48,7 @@ import net.ivpn.core.v2.viewmodel.ConnectionViewModel
import net.ivpn.core.v2.viewmodel.IPv6ViewModel
import net.ivpn.core.v2.viewmodel.ServerListFilterViewModel
import net.ivpn.core.v2.viewmodel.ServerListViewModel
+import net.ivpn.core.common.nightmode.OledModeController
import org.slf4j.LoggerFactory
import javax.inject.Inject
@@ -99,6 +100,7 @@ class ServerListFragment : Fragment(),
init()
viewmodel.start(serverType)
binding.lifecycleOwner = this
+ view.post { OledModeController.applyOledToViewTree(view) }
val pingObserver = Observer> { map ->
(binding.recyclerView.adapter as ServerBasedRecyclerViewAdapter).setPings(map)
@@ -110,6 +112,7 @@ class ServerListFragment : Fragment(),
override fun onResume() {
super.onResume()
viewmodel.navigators.add(this)
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onPause() {
diff --git a/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServerListRecyclerViewAdapter.kt b/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServerListRecyclerViewAdapter.kt
index 66a293029..9fa801c29 100644
--- a/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServerListRecyclerViewAdapter.kt
+++ b/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServerListRecyclerViewAdapter.kt
@@ -44,6 +44,7 @@ import net.ivpn.core.v2.serverlist.dialog.Filters
import net.ivpn.core.v2.serverlist.holders.HolderListener
import net.ivpn.core.v2.serverlist.holders.ServerViewHolder
import net.ivpn.core.v2.serverlist.items.ConnectionOption
+import net.ivpn.core.common.nightmode.OledModeController
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@@ -108,6 +109,8 @@ class FavouriteServerListRecyclerViewAdapter(
setPing(holder.binding, server)
holder.bind(server, forbiddenServer, isIPv6BadgeEnabled, filter)
}
+ // Apply OLED colors to recycled/new items
+ OledModeController.applyOledToViewTree(holder.itemView)
}
private fun setPing(binding: ServerItemBinding, server: Server) {
diff --git a/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServersListFragment.kt b/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServersListFragment.kt
index eb1c3bd67..377ba2e22 100644
--- a/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServersListFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/serverlist/favourite/FavouriteServersListFragment.kt
@@ -48,6 +48,7 @@ import net.ivpn.core.v2.viewmodel.ConnectionViewModel
import net.ivpn.core.v2.viewmodel.IPv6ViewModel
import net.ivpn.core.v2.viewmodel.ServerListFilterViewModel
import net.ivpn.core.v2.viewmodel.ServerListViewModel
+import net.ivpn.core.common.nightmode.OledModeController
import org.slf4j.LoggerFactory
import javax.inject.Inject
@@ -106,6 +107,7 @@ class FavouriteServersListFragment : Fragment(), ServerListViewModel.ServerListN
init(view)
viewmodel.start(serverType)
binding.lifecycleOwner = this
+ view.post { OledModeController.applyOledToViewTree(view) }
val pingObserver = Observer> { map ->
(binding.recyclerView.adapter as ServerBasedRecyclerViewAdapter).setPings(map)
@@ -117,6 +119,7 @@ class FavouriteServersListFragment : Fragment(), ServerListViewModel.ServerListN
override fun onResume() {
super.onResume()
viewmodel.navigators.add(this)
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onPause() {
@@ -145,6 +148,13 @@ class FavouriteServersListFragment : Fragment(), ServerListViewModel.ServerListN
binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.setEmptyView(view.findViewById(R.id.empty_view))
viewmodel.favouriteListeners.add(adapter)
+
+ if (OledModeController.isOledModeEnabled()) {
+ val oledBackground = resources.getColor(R.color.oled_background, null)
+ binding.root.setBackgroundColor(oledBackground)
+ binding.recyclerView.setBackgroundColor(oledBackground)
+ view.findViewById(R.id.empty_view)?.setBackgroundColor(oledBackground)
+ }
}
@Deprecated("Deprecated in Java")
diff --git a/core/src/main/java/net/ivpn/core/v2/settings/SettingsFragment.kt b/core/src/main/java/net/ivpn/core/v2/settings/SettingsFragment.kt
index 7cef8e1c9..542a29e55 100644
--- a/core/src/main/java/net/ivpn/core/v2/settings/SettingsFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/settings/SettingsFragment.kt
@@ -44,6 +44,7 @@ import net.ivpn.core.common.billing.addfunds.Plan
import net.ivpn.core.common.extension.findNavControllerSafely
import net.ivpn.core.common.extension.navigate
import net.ivpn.core.common.nightmode.NightMode
+import net.ivpn.core.common.nightmode.OledModeController
import net.ivpn.core.common.nightmode.OnNightModeChangedListener
import net.ivpn.core.rest.data.model.ServerType
import net.ivpn.core.common.utils.ToastUtil
@@ -118,6 +119,7 @@ class SettingsFragment : Fragment(), OnNightModeChangedListener, ColorThemeViewM
super.onViewCreated(view, savedInstanceState)
IVPNApplication.appComponent.provideActivityComponent().create().inject(this)
initViews()
+ view.post { OledModeController.applyOledToViewTree(view) }
}
override fun onResume() {
@@ -129,6 +131,7 @@ class SettingsFragment : Fragment(), OnNightModeChangedListener, ColorThemeViewM
alwaysOnVPN.onResume()
logging.onResume()
colorTheme.onResume()
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onStart() {
@@ -186,6 +189,9 @@ class SettingsFragment : Fragment(), OnNightModeChangedListener, ColorThemeViewM
binding.contentLayout.sectionInterface.colorThemeLayout.setOnClickListener {
openColorThemeDialogue()
}
+ binding.contentLayout.sectionInterface.appIconLayout.setOnClickListener {
+ openAppIconScreen()
+ }
binding.contentLayout.sectionConnectivity.splitTunnelingLayout.setOnClickListener {
if (!account.authenticated.get()) {
openLoginScreen()
@@ -418,6 +424,11 @@ class SettingsFragment : Fragment(), OnNightModeChangedListener, ColorThemeViewM
DialogBuilderK.openDarkModeDialogue(requireContext(), this, colorTheme)
}
+ private fun openAppIconScreen() {
+ val action = SettingsFragmentDirections.actionSettingsFragmentToAppIconFragment()
+ navigate(action)
+ }
+
private fun openSplitTunnelingScreen() {
val action = SettingsFragmentDirections.actionSettingsFragmentToSplitTunnelingFragment()
navigate(action)
@@ -526,7 +537,7 @@ class SettingsFragment : Fragment(), OnNightModeChangedListener, ColorThemeViewM
return
}
AppCompatDelegate.setDefaultNightMode(mode.systemId)
- println("$mode was selected")
+ activity?.recreate()
}
override fun onNightModeCancelClicked() {
diff --git a/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingFragment.kt b/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingFragment.kt
index eeb85116b..0a62a7b2b 100644
--- a/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingFragment.kt
+++ b/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingFragment.kt
@@ -37,6 +37,7 @@ import net.ivpn.core.R
import net.ivpn.core.databinding.FragmentSplitTunnelingBinding
import net.ivpn.core.v2.viewmodel.SplitTunnelingViewModel
import net.ivpn.core.v2.MainActivity
+import net.ivpn.core.common.nightmode.OledModeController
import javax.inject.Inject
class SplitTunnelingFragment : Fragment() {
@@ -64,6 +65,12 @@ class SplitTunnelingFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
initToolbar()
init()
+ view.post { OledModeController.applyOledToViewTree(view) }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ view?.let { OledModeController.applyOledToViewTree(it) }
}
override fun onStart() {
diff --git a/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingRecyclerViewAdapter.kt b/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingRecyclerViewAdapter.kt
index f5319091a..6f4f9d688 100644
--- a/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingRecyclerViewAdapter.kt
+++ b/core/src/main/java/net/ivpn/core/v2/splittunneling/SplitTunnelingRecyclerViewAdapter.kt
@@ -35,6 +35,7 @@ import net.ivpn.core.v2.splittunneling.holder.AppsSearchViewHolder
import net.ivpn.core.v2.splittunneling.items.ApplicationItem
import net.ivpn.core.v2.splittunneling.items.SearchItem
import net.ivpn.core.v2.splittunneling.items.SplitTunnelingItem
+import net.ivpn.core.common.nightmode.OledModeController
import java.util.*
import javax.inject.Inject
@@ -87,6 +88,8 @@ class SplitTunnelingRecyclerViewAdapter @Inject internal constructor()
} else if (holder is AppsSearchViewHolder) {
searchBinding = holder.binding
}
+
+ OledModeController.applyOledToViewTree(holder.itemView)
}
override fun getItemCount(): Int {
diff --git a/core/src/main/java/net/ivpn/core/v2/timepicker/TimePickerActivity.java b/core/src/main/java/net/ivpn/core/v2/timepicker/TimePickerActivity.java
index e3cf67aa4..47be01b97 100644
--- a/core/src/main/java/net/ivpn/core/v2/timepicker/TimePickerActivity.java
+++ b/core/src/main/java/net/ivpn/core/v2/timepicker/TimePickerActivity.java
@@ -27,6 +27,7 @@
import androidx.appcompat.app.AppCompatActivity;
import net.ivpn.core.IVPNApplication;
+import net.ivpn.core.common.nightmode.OledModeController;
import net.ivpn.core.v2.dialog.DialogBuilder;
import net.ivpn.core.vpn.controller.VpnBehaviorController;
@@ -43,6 +44,7 @@ public class TimePickerActivity extends AppCompatActivity implements OnDelayOpti
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ OledModeController.applyOledTheme(this);
IVPNApplication.appComponent.provideActivityComponent().create().inject(this);
super.onCreate(savedInstanceState);
showPredefinedTimePickerDialog();
diff --git a/core/src/main/java/net/ivpn/core/v2/viewmodel/AppIconViewModel.kt b/core/src/main/java/net/ivpn/core/v2/viewmodel/AppIconViewModel.kt
new file mode 100644
index 000000000..52ba6d3b2
--- /dev/null
+++ b/core/src/main/java/net/ivpn/core/v2/viewmodel/AppIconViewModel.kt
@@ -0,0 +1,59 @@
+package net.ivpn.core.v2.viewmodel
+
+/*
+ IVPN Android app
+ https://github.com/ivpn/android-app
+
+ Created by Tamim Hossain.
+ Copyright (c) 2025 IVPN Limited.
+
+ This file is part of the IVPN Android app.
+
+ The IVPN Android app is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ The IVPN Android app is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
+
+ You should have received a copy of the GNU General Public License
+ along with the IVPN Android app. If not, see .
+*/
+
+import androidx.databinding.ObservableField
+import net.ivpn.core.common.appicon.AppIconManager
+import net.ivpn.core.common.appicon.CustomAppIconData
+import net.ivpn.core.common.dagger.ApplicationScope
+import javax.inject.Inject
+
+@ApplicationScope
+class AppIconViewModel @Inject constructor(
+ private val appIconManager: AppIconManager
+) {
+
+ val currentIcon = ObservableField()
+
+ val ivpnIcons: List = CustomAppIconData.entries
+ .filter { it.category == CustomAppIconData.IconCategory.IVPN }
+
+ val discreetIcons: List = CustomAppIconData.entries
+ .filter { it.category == CustomAppIconData.IconCategory.Discreet }
+
+ fun onResume() {
+ currentIcon.set(appIconManager.getCurrentIconData())
+ }
+
+ fun selectIcon(icon: CustomAppIconData) {
+ if (currentIcon.get() != icon) {
+ appIconManager.setNewAppIcon(icon)
+ currentIcon.set(icon)
+ }
+ }
+
+ fun isSelected(icon: CustomAppIconData): Boolean {
+ return currentIcon.get() == icon
+ }
+}
+
diff --git a/core/src/main/java/net/ivpn/core/v2/viewmodel/ProtocolViewModel.java b/core/src/main/java/net/ivpn/core/v2/viewmodel/ProtocolViewModel.java
index d9d92c172..eff8aba0b 100644
--- a/core/src/main/java/net/ivpn/core/v2/viewmodel/ProtocolViewModel.java
+++ b/core/src/main/java/net/ivpn/core/v2/viewmodel/ProtocolViewModel.java
@@ -79,6 +79,10 @@ public class ProtocolViewModel {
public ObservableField wgInfo = new ObservableField<>();
public ObservableField obfuscationType = new ObservableField<>();
+ public ObservableField wireGuardMtu = new ObservableField<>();
+
+ private static final int MTU_LOWER_BOUND = 576;
+ private static final int MTU_UPPER_BOUND = 65535;
private ProtocolNavigator navigator;
private String wireGuardPublicKey;
@@ -186,6 +190,18 @@ private List getAvailablePortsForCurrentObfuscationType() {
return false;
};
+ @SuppressLint("ClickableViewAccessibility")
+ public View.OnTouchListener mtuTouchListener = (view, motionEvent) -> {
+ if (isVpnActive()) {
+ if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
+ navigator.notifyUser(R.string.snackbar_to_change_mtu_disconnect_first,
+ R.string.snackbar_disconnect_first, null);
+ }
+ return true;
+ }
+ return false;
+ };
+
@Inject
ProtocolViewModel(Context context, Settings settings, WireGuardKeyController keyController,
ProtocolController protocolController, VpnBehaviorController vpnBehaviorController,
@@ -212,6 +228,9 @@ private void init() {
obfuscationType.set(settings.getObfuscationType());
+ int mtu = settings.getWireGuardMtu();
+ wireGuardMtu.set(mtu > 0 ? String.valueOf(mtu) : "");
+
wgInfo.set(getWireGuardInfo());
multiHop.set(multiHopController);
}
@@ -293,6 +312,38 @@ void setPort(Port port) {
}
}
+ public boolean isValidMtu(String mtuString) {
+ if (mtuString == null || mtuString.isEmpty()) {
+ // empty is valid that means use default
+ return true;
+ }
+ try {
+ int mtu = Integer.parseInt(mtuString);
+ return mtu == 0 || (mtu >= MTU_LOWER_BOUND && mtu <= MTU_UPPER_BOUND);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public void saveMtu(String mtuString) {
+ LOGGER.info(TAG, "Save MTU: " + mtuString);
+ int mtu = 0;
+ if (mtuString != null && !mtuString.isEmpty()) {
+ try {
+ mtu = Integer.parseInt(mtuString);
+ } catch (NumberFormatException e) {
+ mtu = 0;
+ }
+ }
+ settings.setWireGuardMtu(mtu);
+ wireGuardMtu.set(mtu > 0 ? String.valueOf(mtu) : "");
+ }
+
+ public String getMtuDisplayValue() {
+ int mtu = settings.getWireGuardMtu();
+ return mtu > 0 ? String.valueOf(mtu) : "";
+ }
+
private void onGeneratingError(String error, Throwable throwable) {
LOGGER.error(TAG, "On generating error: " + error, throwable);
dataLoading.set(false);
diff --git a/core/src/main/java/net/ivpn/core/vpn/local/PermissionActivity.java b/core/src/main/java/net/ivpn/core/vpn/local/PermissionActivity.java
index 605e4d6a3..9a12de04e 100644
--- a/core/src/main/java/net/ivpn/core/vpn/local/PermissionActivity.java
+++ b/core/src/main/java/net/ivpn/core/vpn/local/PermissionActivity.java
@@ -32,6 +32,7 @@
import android.util.Log;
import net.ivpn.core.IVPNApplication;
+import net.ivpn.core.common.nightmode.OledModeController;
import net.ivpn.core.v2.dialog.DialogBuilder;
import net.ivpn.core.v2.dialog.Dialogs;
import net.ivpn.core.vpn.controller.VpnBehaviorController;
@@ -51,6 +52,7 @@ public class PermissionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ OledModeController.applyOledTheme(this);
IVPNApplication.appComponent.provideActivityComponent().create().inject(this);
LOGGER.info("onCreate");
super.onCreate(savedInstanceState);
diff --git a/core/src/main/java/net/ivpn/core/vpn/wireguard/ConfigManager.kt b/core/src/main/java/net/ivpn/core/vpn/wireguard/ConfigManager.kt
index a1f5aad71..7ff470b05 100644
--- a/core/src/main/java/net/ivpn/core/vpn/wireguard/ConfigManager.kt
+++ b/core/src/main/java/net/ivpn/core/vpn/wireguard/ConfigManager.kt
@@ -244,6 +244,13 @@ class ConfigManager @Inject constructor(
val dnsString = getDNS(hosts[0])
config.getInterface().setDnsString(dnsString)
+ // set only if configured
+ val mtu = settings.wireGuardMtu
+ if (mtu > 0) {
+ config.getInterface().setMtu(mtu)
+ LOGGER.info("Using custom MTU: $mtu")
+ }
+
val endpoint = if (v2rayController.isV2RayEnabled()) {
v2rayController.getLocalProxyEndpoint()
} else {
diff --git a/core/src/main/res/drawable-v24/ic_launcher_notes_foreground.xml b/core/src/main/res/drawable-v24/ic_launcher_notes_foreground.xml
new file mode 100644
index 000000000..8dad1610b
--- /dev/null
+++ b/core/src/main/res/drawable-v24/ic_launcher_notes_foreground.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable-v24/ic_launcher_notes_monochrome.xml b/core/src/main/res/drawable-v24/ic_launcher_notes_monochrome.xml
new file mode 100644
index 000000000..bc3f4b27e
--- /dev/null
+++ b/core/src/main/res/drawable-v24/ic_launcher_notes_monochrome.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable-v24/ic_launcher_weather_foreground.xml b/core/src/main/res/drawable-v24/ic_launcher_weather_foreground.xml
new file mode 100644
index 000000000..c6a366b3d
--- /dev/null
+++ b/core/src/main/res/drawable-v24/ic_launcher_weather_foreground.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable-v24/ic_launcher_weather_monochrome.xml b/core/src/main/res/drawable-v24/ic_launcher_weather_monochrome.xml
new file mode 100644
index 000000000..b507e12c5
--- /dev/null
+++ b/core/src/main/res/drawable-v24/ic_launcher_weather_monochrome.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable/ic_launcher_calculator_background.xml b/core/src/main/res/drawable/ic_launcher_calculator_background.xml
new file mode 100644
index 000000000..b44c83dc1
--- /dev/null
+++ b/core/src/main/res/drawable/ic_launcher_calculator_background.xml
@@ -0,0 +1,25 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_launcher_calculator_foreground.xml b/core/src/main/res/drawable/ic_launcher_calculator_foreground.xml
new file mode 100644
index 000000000..ce196cf4c
--- /dev/null
+++ b/core/src/main/res/drawable/ic_launcher_calculator_foreground.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable/ic_launcher_calculator_monochrome.xml b/core/src/main/res/drawable/ic_launcher_calculator_monochrome.xml
new file mode 100644
index 000000000..7e78dec7f
--- /dev/null
+++ b/core/src/main/res/drawable/ic_launcher_calculator_monochrome.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/drawable/ic_launcher_notes_background.xml b/core/src/main/res/drawable/ic_launcher_notes_background.xml
new file mode 100644
index 000000000..78ade5923
--- /dev/null
+++ b/core/src/main/res/drawable/ic_launcher_notes_background.xml
@@ -0,0 +1,25 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_launcher_weather_background.xml b/core/src/main/res/drawable/ic_launcher_weather_background.xml
new file mode 100644
index 000000000..44fd28ddb
--- /dev/null
+++ b/core/src/main/res/drawable/ic_launcher_weather_background.xml
@@ -0,0 +1,25 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/layout/content_app_icon.xml b/core/src/main/res/layout/content_app_icon.xml
new file mode 100644
index 000000000..e8940f9e0
--- /dev/null
+++ b/core/src/main/res/layout/content_app_icon.xml
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/layout/content_wireguard_details.xml b/core/src/main/res/layout/content_wireguard_details.xml
index b7a35ed32..18ee744c2 100644
--- a/core/src/main/res/layout/content_wireguard_details.xml
+++ b/core/src/main/res/layout/content_wireguard_details.xml
@@ -30,7 +30,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- android:paddingTop="?attr/actionBarSize">
+ android:paddingTop="?attr/actionBarSize"
+ android:paddingBottom="80dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/layout/dialog_mtu.xml b/core/src/main/res/layout/dialog_mtu.xml
new file mode 100644
index 000000000..a61839faa
--- /dev/null
+++ b/core/src/main/res/layout/dialog_mtu.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/layout/dialogue_night_mode.xml b/core/src/main/res/layout/dialogue_night_mode.xml
index 8a315e79b..9e2eadf08 100644
--- a/core/src/main/res/layout/dialogue_night_mode.xml
+++ b/core/src/main/res/layout/dialogue_night_mode.xml
@@ -63,6 +63,19 @@
app:buttonTint="@color/dialogue_button"
tools:ignore="RtlSymmetry" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/layout/settings_section_interface.xml b/core/src/main/res/layout/settings_section_interface.xml
index ccae06213..07e1c3b1a 100644
--- a/core/src/main/res/layout/settings_section_interface.xml
+++ b/core/src/main/res/layout/settings_section_interface.xml
@@ -55,6 +55,27 @@
android:alpha="0.5"
android:layout_marginEnd="@dimen/settings_margin_right"/>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator.xml
new file mode 100644
index 000000000..6e833ea58
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator_round.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator_round.xml
new file mode 100644
index 000000000..6e833ea58
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_calculator_round.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes.xml
new file mode 100644
index 000000000..fc8c1fbe3
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes_round.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes_round.xml
new file mode 100644
index 000000000..fc8c1fbe3
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_notes_round.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather.xml
new file mode 100644
index 000000000..fad38f57f
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather_round.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather_round.xml
new file mode 100644
index 000000000..fad38f57f
--- /dev/null
+++ b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_weather_round.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_calculator.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_calculator.webp
new file mode 100644
index 000000000..9a051fb50
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_calculator.webp differ
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_calculator_round.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_calculator_round.webp
new file mode 100644
index 000000000..9191dbeca
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_calculator_round.webp differ
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_notes.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_notes.webp
new file mode 100644
index 000000000..5df0b4bc6
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_notes.webp differ
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_notes_round.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_notes_round.webp
new file mode 100644
index 000000000..fc4459b1c
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_notes_round.webp differ
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_weather.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_weather.webp
new file mode 100644
index 000000000..b80eb52b6
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_weather.webp differ
diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_weather_round.webp b/core/src/main/res/mipmap-hdpi/ic_launcher_weather_round.webp
new file mode 100644
index 000000000..965fb4f44
Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_weather_round.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_calculator.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_calculator.webp
new file mode 100644
index 000000000..e603edf64
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_calculator.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_calculator_round.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_calculator_round.webp
new file mode 100644
index 000000000..ed4e1a08f
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_calculator_round.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_notes.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_notes.webp
new file mode 100644
index 000000000..f7ce45058
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_notes.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_notes_round.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_notes_round.webp
new file mode 100644
index 000000000..7d95ceec9
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_notes_round.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_weather.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_weather.webp
new file mode 100644
index 000000000..33839218d
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_weather.webp differ
diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_weather_round.webp b/core/src/main/res/mipmap-mdpi/ic_launcher_weather_round.webp
new file mode 100644
index 000000000..d8a115c81
Binary files /dev/null and b/core/src/main/res/mipmap-mdpi/ic_launcher_weather_round.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator.webp
new file mode 100644
index 000000000..82d090771
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator_round.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator_round.webp
new file mode 100644
index 000000000..47c0ff6b7
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_calculator_round.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_notes.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_notes.webp
new file mode 100644
index 000000000..bfc7357ab
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_notes.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_notes_round.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_notes_round.webp
new file mode 100644
index 000000000..7aea64d3b
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_notes_round.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_weather.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_weather.webp
new file mode 100644
index 000000000..a8637053f
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_weather.webp differ
diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_weather_round.webp b/core/src/main/res/mipmap-xhdpi/ic_launcher_weather_round.webp
new file mode 100644
index 000000000..46897cd23
Binary files /dev/null and b/core/src/main/res/mipmap-xhdpi/ic_launcher_weather_round.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator.webp
new file mode 100644
index 000000000..cb14a36fb
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator_round.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator_round.webp
new file mode 100644
index 000000000..beced9702
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_calculator_round.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes.webp
new file mode 100644
index 000000000..5581723a3
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes_round.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes_round.webp
new file mode 100644
index 000000000..2d0752562
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_notes_round.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather.webp
new file mode 100644
index 000000000..abbad770e
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather.webp differ
diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather_round.webp b/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather_round.webp
new file mode 100644
index 000000000..3756bfa93
Binary files /dev/null and b/core/src/main/res/mipmap-xxhdpi/ic_launcher_weather_round.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator.webp
new file mode 100644
index 000000000..b34a07eee
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator_round.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator_round.webp
new file mode 100644
index 000000000..f1e0c4693
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_calculator_round.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes.webp
new file mode 100644
index 000000000..5fcfc35ab
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes_round.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes_round.webp
new file mode 100644
index 000000000..a0e89a21c
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_notes_round.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather.webp
new file mode 100644
index 000000000..1f8da66bf
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather.webp differ
diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather_round.webp b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather_round.webp
new file mode 100644
index 000000000..88a55c633
Binary files /dev/null and b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_weather_round.webp differ
diff --git a/core/src/main/res/navigation/nav_graph.xml b/core/src/main/res/navigation/nav_graph.xml
index bf4b1d927..2f5208ef9 100644
--- a/core/src/main/res/navigation/nav_graph.xml
+++ b/core/src/main/res/navigation/nav_graph.xml
@@ -76,6 +76,9 @@
+
+
\ No newline at end of file
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index e08729bcb..6011aa18c 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -206,4 +206,9 @@
#E0E5EA
#2AD489
+ #000000
+ #000000
+ #0A0A0A
+ #80FFFFFF
+
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index afa6acbb1..94672771c 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -42,6 +42,7 @@
Privacy policy
VERSION
Color theme
+ App icon
Enable logging
Multi-hop connection
Multi-hop same provider restriction
@@ -88,6 +89,7 @@
To use split tunneling, please disconnect first
To change protocol, please disconnect first
To change port please disconnect first
+ To change MTU please disconnect first
To change Multi-hop settings please disconnect first
Multi-hop is not available for WireGuard right now
Connection to server blocked, reconnecting on a different port. Please wait until connection is established.
@@ -111,6 +113,7 @@
Light
Dark
+ AMOLED Black
System default
By battery
@@ -456,6 +459,7 @@
Color Theme
Light
Dark
+ AMOLED Black
System default
Set by Battery Saver. Dark mode is used when the system\'s Battery Saver feature is enabled. Otherwise - Light mode.
@@ -535,6 +539,13 @@
Disabled
Quantum Resistance: Indicates whether your current WireGuard VPN connection is using additional protection measures against potential future quantum computer attacks.\n\nWhen Enabled, a Pre-shared key has been securely exchanged between your device and the server using post-quantum Key Encapsulation Mechanism (KEM) algorithms. If Disabled, the current VPN connection, while secure under today\'s standards, does not include this extra layer of quantum resistance.
+ MTU
+ Configure MTU
+ Valid range [576 - 65535]. Please note that changing this value may affect the proper functioning of the VPN tunnel.
+ 576 - 65535
+ Leave blank to use default value
+ Expected value: [576 - 65535]
+
We recommend the WireGuard protocol for its speed, security and connection reliability. For more information visit our protocol comparison web page.
Connect
@@ -660,4 +671,17 @@
V2Ray (VMESS/QUIC)
V2Ray (VMESS/TCP)
VPN obfuscation is a technique that masks VPN traffic to make it appear like standard internet traffic, helping to evade detection and bypass internet restrictions or censorship.
+
+
+ Default
+ Weather
+ Notes
+ Calculator
+ Customize how the IVPN app appears on your device. Discreet icons can help disguise the presence of a VPN app on your phone.
+ Discreet app icon
+ Using a discreet icon changes the app name on the home screen to match the icon.
+ Change app icon?
+ This will cause the IVPN app to close. To restart IVPN, just tap your new app icon.
+ Change
+ Cancel
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
index 4dcde9b1b..d60ff5971 100644
--- a/core/src/main/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
@@ -291,4 +291,51 @@
- true
- 14sp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/store/src/main/java/net/ivpn/client/billing/BillingActivity.java b/store/src/main/java/net/ivpn/client/billing/BillingActivity.java
index 3d79df4e1..5ebe5cbab 100644
--- a/store/src/main/java/net/ivpn/client/billing/BillingActivity.java
+++ b/store/src/main/java/net/ivpn/client/billing/BillingActivity.java
@@ -32,6 +32,7 @@
import net.ivpn.client.R;
import net.ivpn.client.StoreIVPNApplication;
import net.ivpn.client.databinding.ActivityBillingBinding;
+import net.ivpn.core.common.nightmode.OledModeController;
import net.ivpn.core.v2.dialog.DialogBuilder;
import org.slf4j.Logger;
@@ -49,6 +50,7 @@ public class BillingActivity extends AppCompatActivity implements BillingNavigat
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ OledModeController.applyOledTheme(this);
super.onCreate(savedInstanceState);
LOGGER.info("onCreate");
StoreIVPNApplication.instance.billingComponent.inject(this);