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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty/glean
Submodule glean updated from 9f785a to 3bfe35
2 changes: 1 addition & 1 deletion 3rdparty/i18n
Submodule i18n updated 106 files
1 change: 1 addition & 0 deletions android/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>

<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.firefox.qt.common

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi

/**
* Helper class for managing battery optimization settings.
*
* Battery optimization can cause Android to kill VPN services after a few hours
* or prevent them from starting after device reboot. This helper provides methods
* to detect battery optimization status and guide users to disable it.
*/
object BatteryOptimizationHelper {
private const val TAG = "BatteryOptimizationHelper"

/**
* Check if battery optimizations are disabled for this app.
*
* @param context Application context
* @return true if optimizations are disabled (good for VPN), false if enabled
*/
@JvmStatic
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Battery optimization was introduced in Android M (API 23)
return true
}

return try {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(context.packageName)
} catch (e: Exception) {
Log.e(TAG, "Failed to check battery optimization status", e)
// Assume it's okay if we can't check (fail-safe)
true
}
}

/**
* Check if we have permission to request battery optimization exemption.
*
* @param context Application context
* @return true if permission is granted
*/
@JvmStatic
fun hasRequestIgnoreBatteryOptimizationsPermission(context: Context): Boolean {
return try {
val permission = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"
context.packageManager.checkPermission(permission, context.packageName) ==
PackageManager.PERMISSION_GRANTED
} catch (e: Exception) {
Log.e(TAG, "Failed to check REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission", e)
false
}
}

/**
* Get intent to request battery optimization exemption or open settings.
*
* This method returns different intents based on:
* - Whether we have REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission
* - Android version
*
* @param context Application context
* @return Intent to open appropriate settings screen, or null if failed
*/
@RequiresApi(Build.VERSION_CODES.M)
@JvmStatic
fun getRequestIgnoreBatteryOptimizationsIntent(context: Context): Intent? {
return try {
if (hasRequestIgnoreBatteryOptimizationsPermission(context)) {
// Can request directly via system dialog
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:${context.packageName}")
intent
} else {
// Need to guide user to settings manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+: Go to app details
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${context.packageName}")
}
} else {
// Android 6-11: Go to battery optimization list
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to create battery optimization intent", e)
null
}
}

/**
* Check if background restrictions are enabled for this app.
* Background restrictions prevent apps from running in the background.
*
* @param context Application context
* @return true if restrictions are enabled (bad for VPN), false otherwise
*/
@RequiresApi(Build.VERSION_CODES.P)
fun isBackgroundRestricted(context: Context): Boolean {
return try {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
activityManager.isBackgroundRestricted
} catch (e: Exception) {
Log.e(TAG, "Failed to check background restrictions", e)
false
}
}

/**
* Check if background data is restricted for this app.
* Background data restrictions prevent apps from using data in the background.
*
* @param context Application context
* @return true if data is restricted (bad for VPN), false otherwise
*/
@RequiresApi(Build.VERSION_CODES.N)
fun isBackgroundDataRestricted(context: Context): Boolean {
return try {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
connectivityManager.restrictBackgroundStatus == android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
} catch (e: Exception) {
Log.e(TAG, "Failed to check background data restrictions", e)
false
}
}

/**
* Get a user-friendly description of why battery optimization should be disabled.
*
* @return String explaining the issue
*/
@JvmStatic
fun getBatteryOptimizationExplanation(): String {
return "Mozilla VPN may disconnect after a few hours and won't restart after device reboot " +
"because battery optimization is enabled.\n\n" +
"For reliable VPN operation, please disable battery optimization for Mozilla VPN.\n\n" +
"This allows the VPN to maintain your secure connection continuously."
}

/**
* Get a short warning message for battery optimization.
*
* @return Short warning string
*/
fun getBatteryOptimizationWarning(): String {
return "⚠️ Battery optimization is enabled - VPN may disconnect"
}

/**
* Log battery optimization status and related restrictions.
* Useful for troubleshooting.
*
* @param context Application context
* @param tag Log tag to use
*/
fun logBatteryOptimizationStatus(context: Context, tag: String = TAG) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val isIgnoring = isIgnoringBatteryOptimizations(context)
if (isIgnoring) {
Log.i(tag, "✓ Battery optimization is disabled - VPN should remain stable")
} else {
Log.w(tag, "⚠️ Battery optimization is ENABLED - VPN may disconnect after a few hours!")
Log.w(tag, "⚠️ User should disable battery optimization for reliable VPN operation")
}

// Check background restrictions on Android P+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (isBackgroundRestricted(context)) {
Log.w(tag, "⚠️ Background restrictions are enabled - VPN may not work properly!")
}
}

// Check background data restrictions on Android N+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (isBackgroundDataRestricted(context)) {
Log.w(tag, "⚠️ Background data is restricted - VPN may not work properly!")
}
}
} else {
Log.i(tag, "Battery optimization checks not available on Android < 6.0")
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,93 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import org.mozilla.firefox.qt.common.BatteryOptimizationHelper
import org.mozilla.firefox.qt.common.Prefs

/**
* Boot receiver that starts the VPN service when the device boots.
*
* IMPORTANT: This receiver may fail to start the VPN service if battery optimization
* is enabled, especially on Android 12+ (API 31+). Battery optimization should be
* disabled for reliable VPN auto-start after reboot.
*/
class BootReceiver : BroadcastReceiver() {
private val TAG = "BootReceiver"

override fun onReceive(context: Context, arg1: Intent) {
Log.init(context)

// Check if start on boot is enabled
if (!Prefs.get(context).getBoolean(START_ON_BOOT, false)) {
Log.i(TAG, "This device did not enable start on boot - exit")
Log.i(TAG, "Start on boot is disabled - exit")
return
}
Log.i(TAG, "This device did enable start on boot - try to start")

Log.i(TAG, "Device rebooted - attempting to start VPN service")

// Check battery optimization status and log detailed information
checkBatteryOptimizationStatus(context)

// Attempt to start the VPN service
val intent = Intent(context, VPNService::class.java)
intent.putExtra("startOnBoot", true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
Log.i(TAG, "✓ Successfully queued VPN service start (foreground)")
} else {
context.startService(intent)
Log.i(TAG, "✓ Successfully started VPN service")
}
} catch (e: IllegalStateException) {
// This exception commonly happens on Android 12+ when battery optimization is enabled
Log.e(TAG, "❌ Failed to start VPN service from background!")
Log.e(TAG, "❌ This is likely because battery optimization is enabled")
Log.e(TAG, "❌ VPN will not auto-start until user opens the app")
Log.e(TAG, "Exception: ${e.message}")
Log.stack(TAG, e.stackTrace)

// Note: We cannot show a notification here reliably on Android 12+
// The user will need to open the app, which will show the battery optimization warning
} catch (e: SecurityException) {
// Permission denied
Log.e(TAG, "❌ Security exception when starting VPN service")
Log.e(TAG, "❌ VPN permissions may have been revoked")
Log.e(TAG, "Exception: ${e.message}")
Log.stack(TAG, e.stackTrace)
} catch (e: Exception) {
// Other unexpected errors
Log.e(TAG, "❌ Unexpected error starting VPN service: ${e.message}")
Log.stack(TAG, e.stackTrace)
}
}

/**
* Check and log battery optimization status.
* This helps diagnose why VPN auto-start may fail after reboot.
*/
private fun checkBatteryOptimizationStatus(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BatteryOptimizationHelper.logBatteryOptimizationStatus(context, TAG)

val isIgnoring = BatteryOptimizationHelper.isIgnoringBatteryOptimizations(context)
if (!isIgnoring) {
Log.e(TAG, "⚠️ Battery optimization is enabled - VPN may not start from background!")
Log.e(TAG, "⚠️ This is especially problematic on Android 12+ (API 31+)")
Log.e(TAG, "⚠️ User should open the app and disable battery optimization")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Log.e(TAG, "⚠️ Android 12+ detected - background service start restrictions apply")
}
} else {
Log.i(TAG, "✓ Battery optimization is disabled - VPN should start successfully")
}
} else {
context.startService(intent)
Log.i(TAG, "Battery optimization checks not available on Android < 6.0")
}
Log.i(TAG, "Queued VPNService start")
}

companion object {
const val START_ON_BOOT = "startOnBoot"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import mozilla.telemetry.glean.BuildInfo
import mozilla.telemetry.glean.Glean
import mozilla.telemetry.glean.config.Configuration
import org.json.JSONObject
import org.mozilla.firefox.qt.common.BatteryOptimizationHelper
import org.mozilla.firefox.qt.common.CoreBinder
import org.mozilla.firefox.qt.common.Prefs
import org.mozilla.firefox.vpn.daemon.GleanMetrics.ConnectionHealth
Expand Down Expand Up @@ -147,6 +148,9 @@ class VPNService : android.net.VpnService() {
Log.i(tag, "Wireguard reported current tunnel: $currentTunnelHandle")
mAlreadyInitialised = true

// Check battery optimization status and log warnings
checkBatteryOptimizationStatus()

// It's usually a bad practice to initialize Glean with the wrong
// value for uploadEnabled... However, since this is a very controlled
// situation -- it should only happen when logging in to a brand new
Expand All @@ -159,6 +163,26 @@ class VPNService : android.net.VpnService() {
initializeGlean(Prefs.get(this).getBoolean("glean_enabled", false))
}

/**
* Check battery optimization status and log warnings.
* This helps users understand why VPN may disconnect after a few hours.
*/
private fun checkBatteryOptimizationStatus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BatteryOptimizationHelper.logBatteryOptimizationStatus(this, tag)

// If battery optimization is enabled, send broadcast to notify UI
if (!BatteryOptimizationHelper.isIgnoringBatteryOptimizations(this)) {
try {
val intent = Intent("org.mozilla.firefox.vpn.BATTERY_OPTIMIZATION_WARNING")
sendBroadcast(intent)
} catch (e: Exception) {
Log.e(tag, "Failed to send battery optimization warning broadcast: ${e.message}")
}
}
}
}

override fun onUnbind(intent: Intent?): Boolean {
if (!isUp) {
Log.v(tag, "Client Disconnected, VPN is down - Service might shut down soon")
Expand Down Expand Up @@ -360,6 +384,13 @@ class VPNService : android.net.VpnService() {
// asked boot vpn from the OS
val prefs = Prefs.get(this)
prefs.edit().putString("lastConf", json.toString()).apply()

// Auto-enable start on boot when VPN connects successfully
// This ensures VPN will restart after device reboot
if (!prefs.getBoolean(BootReceiver.START_ON_BOOT, false)) {
Log.i(tag, "Auto-enabling start on boot for reliable VPN restart after reboot")
prefs.edit().putBoolean(BootReceiver.START_ON_BOOT, true).apply()
}

// Go foreground
CannedNotification(mConfig)?.let { mNotificationHandler.show(it) }
Expand Down
2 changes: 1 addition & 1 deletion android/tunnel/src/go/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ file(MAKE_DIRECTORY "${OUTPUT_LIB_DIR}")
add_custom_command(
OUTPUT "${OUTPUT_LIB_DIR}/libtunnel.so"
COMMAND ${CMAKE_COMMAND} -E env ${GO_ENV}
${GO_EXECUTABLE} build -buildmode=c-shared
${GO_EXECUTABLE} build -buildmode=c-shared -buildvcs=false
-o "${OUTPUT_LIB_DIR}/libtunnel.so"
"${GO_SRC}"
WORKING_DIRECTORY "${GO_SRC}"
Expand Down
Loading
Loading