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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,68 @@
<activity
android:name="net.ivpn.core.v2.MainActivity"
android:screenOrientation="portrait"
android:exported="true"
android:exported="false"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustNothing"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>

<!-- Activity aliases for app icon switching -->
<activity-alias
android:name="net.ivpn.client.MainActivity"
android:targetActivity="net.ivpn.core.v2.MainActivity"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity-alias
android:name="net.ivpn.client.MainActivityWeather"
android:targetActivity="net.ivpn.core.v2.MainActivity"
android:label="@string/app_icon_name_weather"
android:icon="@mipmap/ic_launcher_weather"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity-alias
android:name="net.ivpn.client.MainActivityAlarmClock"
android:targetActivity="net.ivpn.core.v2.MainActivity"
android:label="@string/app_icon_name_alarm_clock"
android:icon="@mipmap/ic_launcher_alarm_clock"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity-alias
android:name="net.ivpn.client.MainActivityCalculator"
android:targetActivity="net.ivpn.core.v2.MainActivity"
android:label="@string/app_icon_name_calculator"
android:icon="@mipmap/ic_launcher_calculator"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity
android:name="net.ivpn.core.vpn.local.PermissionActivity"
android:exported="false"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added core/src/main/ic_launcher_notes-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added core/src/main/ic_launcher_weather-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private void setListenPortString(@Nullable final String port) {
setListenPort(0);
}

private void setMtu(final int mtu) {
public void setMtu(final int mtu) {
this.mtu = mtu;
}

Expand Down
97 changes: 97 additions & 0 deletions core/src/main/java/net/ivpn/core/common/appicon/AppIconManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package net.ivpn.core.common.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 <https://www.gnu.org/licenses/>.
*/

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),
ALARM_CLOCK(".MainActivityAlarmClock", R.mipmap.ic_launcher_alarm_clock, R.string.app_icon_name_alarm_clock, 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
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -156,6 +157,8 @@ interface Factory {

void inject(KillSwitchFragment fragment);

void inject(AppIconFragment fragment);

void inject(MockLocationFragment fragment);

void inject(MockLocationStep1Fragment fragment);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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
}
}
}

Loading