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);