Native android service implemented

This commit is contained in:
pokamest 2021-09-30 15:56:48 +03:00
parent d553d7f772
commit eb497be730
81 changed files with 6442 additions and 34 deletions

View file

@ -0,0 +1,115 @@
/* 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.amnezia.vpn
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Parcel
import androidx.core.app.NotificationCompat
import org.json.JSONObject
object NotificationUtil {
var sCurrentContext: Context? = null
private var sNotificationBuilder: NotificationCompat.Builder? = null
const val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
const val CONNECTED_NOTIFICATION_ID = 1337
const val tag = "NotificationUtil"
/**
* Updates the current shown notification from a
* Parcel - Gets called from AndroidController.cpp
*/
fun update(data: Parcel) {
// [data] is here a json containing the noification content
val buffer = data.createByteArray()
val json = buffer?.let { String(it) }
val content = JSONObject(json)
update(content.getString("title"), content.getString("message"))
}
/**
* Updates the current shown notification
*/
fun update(heading: String, message: String) {
if (sCurrentContext == null) return
val notificationManager: NotificationManager =
sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
sNotificationBuilder?.let {
it.setContentTitle(heading)
.setContentText(message)
notificationManager.notify(CONNECTED_NOTIFICATION_ID, it.build())
}
}
/**
* Saves the default translated "connected" notification, in case the vpn gets started
* without the app.
*/
fun saveFallBackMessage(data: Parcel, context: Context) {
// [data] is here a json containing the notification content
val buffer = data.createByteArray()
val json = buffer?.let { String(it) }
val content = JSONObject(json)
val prefs = Prefs.get(context)
prefs.edit()
.putString("fallbackNotificationHeader", content.getString("title"))
.putString("fallbackNotificationMessage", content.getString("message"))
.apply()
Log.v(tag, "Saved new fallback message -> ${content.getString("title")}")
}
/*
* Creates a new Notification using the current set of Strings
* Shows the notification in the given {context}
*/
fun show(service: VPNService) {
sNotificationBuilder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
sCurrentContext = service
val notificationManager: NotificationManager =
sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// From Oreo on we need to have a "notification channel" to post to.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "vpn"
val descriptionText = " "
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
notificationManager.createNotificationChannel(channel)
}
// In case we do not have gotten a message to show from the Frontend
// try to populate the notification with a translated Fallback message
val prefs = Prefs.get(service)
val message =
"" + prefs.getString("fallbackNotificationMessage", "Running in the Background")
val header = "" + prefs.getString("fallbackNotificationHeader", "Mozilla VPN")
// Create the Intent that Should be Fired if the User Clicks the notification
val mainActivityName = "org.amnezia.vpn.qt.VPNActivity"
val activity = Class.forName(mainActivityName)
val intent = Intent(service, activity)
val pendingIntent = PendingIntent.getActivity(service, 0, intent, 0)
// Build our notification
sNotificationBuilder?.let {
it.setSmallIcon(org.amnezia.vpn.R.drawable.ic_amnezia_round)
.setContentTitle(header)
.setContentText(message)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
service.startForeground(CONNECTED_NOTIFICATION_ID, it.build())
}
}
}

View file

@ -0,0 +1,35 @@
/* 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.amnezia.vpn
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
object Prefs {
// Opens and returns an instance of EncryptedSharedPreferences
fun get(context: Context): SharedPreferences {
try {
val mainKey = MasterKey.Builder(context.applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPrefsFile = "com.amnezia.vpn.secure.prefs"
val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
context.applicationContext,
sharedPrefsFile,
mainKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
return sharedPreferences
} catch (e: Exception) {
Log.e("Android-Prefs", "Getting Encryption Storage failed, plaintext fallback")
return context.getSharedPreferences("com.amnezia.vpn.prefrences", Context.MODE_PRIVATE)
}
}
}

View file

@ -0,0 +1,72 @@
/* 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.amnezia.vpn
import android.content.Context
import java.io.File
import java.time.LocalDateTime
import android.util.Log as nativeLog
/*
* Drop in replacement for android.util.Log
* Also stores a copy of all logs in tmp/mozilla_deamon_logs.txt
*/
class Log {
val LOG_MAX_FILE_SIZE = 204800
private var file: File
private constructor(context: Context) {
val tempDIR = context.cacheDir
file = File(tempDIR, "mozilla_deamon_logs.txt")
if (file.length() > LOG_MAX_FILE_SIZE) {
file.writeText("")
}
}
companion object {
var instance: Log? = null
fun init(ctx: Context) {
if (instance == null) {
instance = Log(ctx)
}
}
fun i(tag: String, message: String) {
instance?.write("[info] - ($tag) - $message")
if (!BuildConfig.DEBUG) { return; }
nativeLog.i(tag, message)
}
fun v(tag: String, message: String) {
instance?.write("($tag) - $message")
if (!BuildConfig.DEBUG) { return; }
nativeLog.v(tag, message)
}
fun e(tag: String, message: String) {
instance?.write("[error] - ($tag) - $message")
if (!BuildConfig.DEBUG) { return; }
nativeLog.e(tag, message)
}
// Only Prints && Loggs when in debug, noop in release.
fun sensitive(tag: String, message: String?) {
if (!BuildConfig.DEBUG) { return; }
if (message == null) { return; }
e(tag, message)
}
fun getContent(): String? {
return try {
instance?.file?.readText()
} catch (e: Exception) {
"=== Failed to read Daemon Logs === \n ${e.localizedMessage} "
}
}
fun clearFile() {
instance?.file?.writeText("")
}
}
private fun write(message: String) {
LocalDateTime.now()
file.appendText("[${LocalDateTime.now()}] $message \n")
}
}

View file

@ -0,0 +1,309 @@
/* 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.amnezia.vpn
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.system.OsConstants
import com.wireguard.android.util.SharedLibraryLoader
import com.wireguard.config.*
import com.wireguard.crypto.Key
import org.json.JSONObject
class VPNService : android.net.VpnService() {
private val tag = "VPNService"
private var mBinder: VPNServiceBinder = VPNServiceBinder(this)
private var mConfig: JSONObject? = null
private var mConnectionTime: Long = 0
private var mAlreadyInitialised = false
private var currentTunnelHandle = -1
fun init() {
if (mAlreadyInitialised) {
return
}
Log.init(this)
SharedLibraryLoader.loadSharedLibrary(this, "wg-go")
Log.i(tag, "loaded lib")
Log.e(tag, "Wireguard Version ${wgVersion()}")
mAlreadyInitialised = true
}
override fun onUnbind(intent: Intent?): Boolean {
if (!isUp) {
// If the Qt Client got closed while we were not connected
// we do not need to stay as a foreground service.
stopForeground(true)
}
return super.onUnbind(intent)
}
/**
* EntryPoint for the Service, gets Called when AndroidController.cpp
* calles bindService. Returns the [VPNServiceBinder] so QT can send Requests to it.
*/
override fun onBind(intent: Intent?): IBinder? {
Log.v(tag, "Got Bind request")
init()
return mBinder
}
/**
* Might be the entryPoint if the Service gets Started via an
* Service Intent: Might be from Always-On-Vpn from Settings
* or from Booting the device and having "connect on boot" enabled.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
init()
intent?.let {
if (intent.getBooleanExtra("startOnly", false)) {
Log.i(tag, "Start only!")
return super.onStartCommand(intent, flags, startId)
}
}
// This start is from always-on
if (this.mConfig == null) {
// We don't have tunnel to turn on - Try to create one with last config the service got
val prefs = Prefs.get(this)
val lastConfString = prefs.getString("lastConf", "")
if (lastConfString.isNullOrEmpty()) {
// We have nothing to connect to -> Exit
Log.e(
tag,
"VPN service was triggered without defining a Server or having a tunnel"
)
return super.onStartCommand(intent, flags, startId)
}
this.mConfig = JSONObject(lastConfString)
}
turnOn(this.mConfig!!)
return super.onStartCommand(intent, flags, startId)
}
// Invoked when the application is revoked.
// At this moment, the VPN interface is already deactivated by the system.
override fun onRevoke() {
this.turnOff()
super.onRevoke()
}
var connectionTime: Long = 0
get() {
return mConnectionTime
}
var isUp: Boolean
get() {
return currentTunnelHandle >= 0
}
private set(value) {
if (value) {
mBinder.dispatchEvent(VPNServiceBinder.EVENTS.connected, "")
mConnectionTime = System.currentTimeMillis()
return
}
mBinder.dispatchEvent(VPNServiceBinder.EVENTS.disconnected, "")
mConnectionTime = 0
}
val status: JSONObject
get() {
val deviceIpv4: String = ""
return JSONObject().apply {
putOpt("rx_bytes", getConfigValue("rx_bytes"))
putOpt("tx_bytes", getConfigValue("tx_bytes"))
putOpt("endpoint", mConfig?.getJSONObject("server")?.getString("ipv4Gateway"))
putOpt("deviceIpv4", mConfig?.getJSONObject("device")?.getString("ipv4Address"))
}
}
/*
* Checks if the VPN Permission is given.
* If the permission is given, returns true
* Requests permission and returns false if not.
*/
fun checkPermissions(): Boolean {
// See https://developer.android.com/guide/topics/connectivity/vpn#connect_a_service
// Call Prepare, if we get an Intent back, we dont have the VPN Permission
// from the user. So we need to pass this to our main Activity and exit here.
val intent = prepare(this)
if (intent == null) {
Log.e(tag, "VPN Permission Already Present")
return true
}
Log.e(tag, "Requesting VPN Permission")
return false
}
fun turnOn(json: JSONObject) {
Log.sensitive(tag, json.toString())
val wireguard_conf = buildWireugardConfig(json)
if (!checkPermissions()) {
Log.e(tag, "turn on was called without no permissions present!")
isUp = false
return
}
Log.i(tag, "Permission okay")
if (currentTunnelHandle != -1) {
Log.e(tag, "Tunnel already up")
// Turn the tunnel down because this might be a switch
wgTurnOff(currentTunnelHandle)
}
val wgConfig: String = wireguard_conf!!.toWgUserspaceString()
val builder = Builder()
setupBuilder(wireguard_conf, builder)
builder.setSession("mvpn0")
builder.establish().use { tun ->
if (tun == null)return
Log.i(tag, "Go backend " + wgVersion())
currentTunnelHandle = wgTurnOn("mvpn0", tun.detachFd(), wgConfig)
}
if (currentTunnelHandle < 0) {
Log.e(tag, "Activation Error Code -> $currentTunnelHandle")
isUp = false
return
}
protect(wgGetSocketV4(currentTunnelHandle))
protect(wgGetSocketV6(currentTunnelHandle))
mConfig = json
isUp = true
// Store the config in case the service gets
// asked boot vpn from the OS
val prefs = Prefs.get(this)
prefs.edit()
.putString("lastConf", json.toString())
.apply()
NotificationUtil.show(this) // Go foreground
}
fun turnOff() {
Log.v(tag, "Try to disable tunnel")
wgTurnOff(currentTunnelHandle)
currentTunnelHandle = -1
stopForeground(false)
isUp = false
}
/**
* Configures an Android VPN Service Tunnel
* with a given Wireguard Config
*/
private fun setupBuilder(config: Config, builder: Builder) {
// Setup Split tunnel
for (excludedApplication in config.`interface`.excludedApplications)
builder.addDisallowedApplication(excludedApplication)
// Device IP
for (addr in config.`interface`.addresses) builder.addAddress(addr.address, addr.mask)
// DNS
for (addr in config.`interface`.dnsServers) builder.addDnsServer(addr.hostAddress)
// Add All routes the VPN may route tos
for (peer in config.peers) {
for (addr in peer.allowedIps) {
builder.addRoute(addr.address, addr.mask)
}
}
builder.allowFamily(OsConstants.AF_INET)
builder.allowFamily(OsConstants.AF_INET6)
builder.setMtu(config.`interface`.mtu.orElse(1280))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) setUnderlyingNetworks(null)
builder.setBlocking(true)
}
/**
* Gets config value for {key} from the Current
* running Wireguard tunnel
*/
private fun getConfigValue(key: String): String? {
if (!isUp) {
return null
}
val config = wgGetConfig(currentTunnelHandle) ?: return null
val lines = config.split("\n")
for (line in lines) {
val parts = line.split("=")
val k = parts.first()
val value = parts.last()
if (key == k) {
return value
}
}
return null
}
/**
* Create a Wireguard [Config] from a [json] string -
* The [json] will be created in AndroidController.cpp
*/
private fun buildWireugardConfig(obj: JSONObject): Config {
val confBuilder = Config.Builder()
val jServer = obj.getJSONObject("server")
val peerBuilder = Peer.Builder()
val ep =
InetEndpoint.parse(jServer.getString("ipv4AddrIn") + ":" + jServer.getString("port"))
peerBuilder.setEndpoint(ep)
peerBuilder.setPublicKey(Key.fromBase64(jServer.getString("publicKey")))
val jAllowedIPList = obj.getJSONArray("allowedIPs")
if (jAllowedIPList.length() == 0) {
val internet = InetNetwork.parse("0.0.0.0/0") // aka The whole internet.
peerBuilder.addAllowedIp(internet)
} else {
(0 until jAllowedIPList.length()).toList().forEach {
val network = InetNetwork.parse(jAllowedIPList.getString(it))
peerBuilder.addAllowedIp(network)
}
}
confBuilder.addPeer(peerBuilder.build())
val privateKey = obj.getJSONObject("keys").getString("privateKey")
val jDevice = obj.getJSONObject("device")
val ifaceBuilder = Interface.Builder()
ifaceBuilder.parsePrivateKey(privateKey)
ifaceBuilder.addAddress(InetNetwork.parse(jDevice.getString("ipv4Address")))
ifaceBuilder.addAddress(InetNetwork.parse(jDevice.getString("ipv6Address")))
ifaceBuilder.addDnsServer(InetNetwork.parse(obj.getString("dns")).address)
val jExcludedApplication = obj.getJSONArray("excludedApps")
(0 until jExcludedApplication.length()).toList().forEach {
val appName = jExcludedApplication.get(it).toString()
ifaceBuilder.excludeApplication(appName)
}
confBuilder.setInterface(ifaceBuilder.build())
return confBuilder.build()
}
companion object {
@JvmStatic
fun startService(c: Context) {
c.applicationContext.startService(
Intent(c.applicationContext, VPNService::class.java).apply {
putExtra("startOnly", true)
}
)
}
@JvmStatic
private external fun wgGetConfig(handle: Int): String?
@JvmStatic
private external fun wgGetSocketV4(handle: Int): Int
@JvmStatic
private external fun wgGetSocketV6(handle: Int): Int
@JvmStatic
private external fun wgTurnOff(handle: Int)
@JvmStatic
private external fun wgTurnOn(ifName: String, tunFd: Int, settings: String): Int
@JvmStatic
private external fun wgVersion(): String?
}
}

View file

@ -0,0 +1,170 @@
/* 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.amnezia.vpn
import android.os.Binder
import android.os.DeadObjectException
import android.os.IBinder
import android.os.Parcel
import com.wireguard.config.*
import org.json.JSONObject
import java.lang.Exception
class VPNServiceBinder(service: VPNService) : Binder() {
private val mService = service
private val tag = "VPNServiceBinder"
private var mListener: IBinder? = null
private var mResumeConfig: JSONObject? = null
/**
* The codes this Binder does accept in [onTransact]
*/
object ACTIONS {
const val activate = 1
const val deactivate = 2
const val registerEventListener = 3
const val requestStatistic = 4
const val requestGetLog = 5
const val requestCleanupLog = 6
const val resumeActivate = 7
const val setNotificationText = 8
const val setFallBackNotification = 9
}
/**
* Gets called when the VPNServiceBinder gets a request from a Client.
* The [code] determines what action is requested. - see [ACTIONS]
* [data] may contain a utf-8 encoded json string with optional args or is null.
* [reply] is a pointer to a buffer in the clients memory, to reply results.
* we use this to send result data.
*
* returns true if the [code] was accepted
*/
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
Log.i(tag, "GOT TRANSACTION $code")
when (code) {
ACTIONS.activate -> {
try {
Log.i(tag, "Activiation Requested, parsing Config")
// [data] is here a json containing the wireguard conf
val buffer = data.createByteArray()
val json = buffer?.let { String(it) }
val config = JSONObject(json)
Log.v(tag, "Stored new Tunnel config in Service")
if (!mService.checkPermissions()) {
mResumeConfig = config
// The Permission prompt was already
// send, in case it's accepted we will
// receive ACTIONS.resumeActivate
return true
}
this.mService.turnOn(config)
} catch (e: Exception) {
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
dispatchEvent(EVENTS.activationError, e.localizedMessage)
}
return true
}
ACTIONS.resumeActivate -> {
// [data] is empty
// Activate the current tunnel
try {
mResumeConfig?.let { this.mService.turnOn(it) }
} catch (e: Exception) {
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
}
return true
}
ACTIONS.deactivate -> {
// [data] here is empty
this.mService.turnOff()
return true
}
ACTIONS.registerEventListener -> {
// [data] contains the Binder that we need to dispatch the Events
val binder = data.readStrongBinder()
mListener = binder
val obj = JSONObject()
obj.put("connected", mService.isUp)
obj.put("time", mService.connectionTime)
dispatchEvent(EVENTS.init, obj.toString())
return true
}
ACTIONS.requestStatistic -> {
dispatchEvent(EVENTS.statisticUpdate, mService.status.toString())
return true
}
ACTIONS.requestGetLog -> {
// Grabs all the Logs and dispatch new Log Event
dispatchEvent(EVENTS.backendLogs, Log.getContent())
return true
}
ACTIONS.requestCleanupLog -> {
Log.clearFile()
return true
}
ACTIONS.setNotificationText -> {
NotificationUtil.update(data)
return true
}
ACTIONS.setFallBackNotification -> {
NotificationUtil.saveFallBackMessage(data, mService)
return true
}
IBinder.LAST_CALL_TRANSACTION -> {
Log.e(tag, "The OS Requested to shut down the VPN")
this.mService.turnOff()
return true
}
else -> {
Log.e(tag, "Received invalid bind request \t Code -> $code")
// If we're hitting this there is probably something wrong in the client.
return false
}
}
return false
}
/**
* Dispatches an Event to all registered Binders
* [code] the Event that happened - see [EVENTS]
* To register an Eventhandler use [onTransact] with
* [ACTIONS.registerEventListener]
*/
fun dispatchEvent(code: Int, payload: String?) {
try {
mListener?.let {
if (it.isBinderAlive) {
val data = Parcel.obtain()
data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
it.transact(code, data, Parcel.obtain(), 0)
}
}
} catch (e: DeadObjectException) {
// If the QT Process is killed (not just inactive)
// we cant access isBinderAlive, so nothing to do here.
}
}
/**
* The codes we Are Using in case of [dispatchEvent]
*/
object EVENTS {
const val init = 0
const val connected = 1
const val disconnected = 2
const val statisticUpdate = 3
const val backendLogs = 4
const val activationError = 5
}
}

View file

@ -0,0 +1,189 @@
/* 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.amnezia.vpn.qt;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.Manifest.permission;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
// Gets used by /platforms/android/androidAppListProvider.cpp
public class PackageManagerHelper {
final static String TAG = "PackageManagerHelper";
final static int MIN_CHROME_VERSION = 65;
final static List<String> CHROME_BROWSERS = Arrays.asList(
new String[] {"com.google.android.webview", "com.android.webview", "com.google.chrome"});
private static String getAllAppNames(Context ctx) {
JSONObject output = new JSONObject();
PackageManager pm = ctx.getPackageManager();
List<String> browsers = getBrowserIDs(pm);
List<PackageInfo> packs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
for (int i = 0; i < packs.size(); i++) {
PackageInfo p = packs.get(i);
// Do not add ourselves and System Apps to the list, unless it might be a browser
if ((!isSystemPackage(p,pm) || browsers.contains(p.packageName))
&& !isSelf(p)) {
String appid = p.packageName;
String appName = p.applicationInfo.loadLabel(pm).toString();
try {
output.put(appid, appName);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
return output.toString();
}
private static Drawable getAppIcon(Context ctx, String id) {
try {
return ctx.getPackageManager().getApplicationIcon(id);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return new ColorDrawable(Color.TRANSPARENT);
}
private static boolean isSystemPackage(PackageInfo pkgInfo, PackageManager pm) {
if( (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){
// no system app
return false;
}
// For Systems Packages there are Cases where we want to add it anyway:
// Has the use Internet permission (otherwise makes no sense)
// Had at least 1 update (this means it's probably on any AppStore)
// Has a a launch activity (has a ui and is not just a system service)
if(!usesInternet(pkgInfo)){
return true;
}
if(!hadUpdate(pkgInfo)){
return true;
}
if(pm.getLaunchIntentForPackage(pkgInfo.packageName) == null){
// If there is no way to launch this from a homescreen, def a sys package
return true;
}
return false;
}
private static boolean isSelf(PackageInfo pkgInfo) {
return pkgInfo.packageName.equals("org.amnezia.vpn")
|| pkgInfo.packageName.equals("org.amnezia.vpn.debug");
}
private static boolean usesInternet(PackageInfo pkgInfo){
if(pkgInfo.requestedPermissions == null){
return false;
}
for(int i=0; i < pkgInfo.requestedPermissions.length; i++) {
String permission = pkgInfo.requestedPermissions[i];
if(Manifest.permission.INTERNET.equals(permission)){
return true;
}
}
return false;
}
private static boolean hadUpdate(PackageInfo pkgInfo){
return pkgInfo.lastUpdateTime > pkgInfo.firstInstallTime;
}
// Returns List of all Packages that can classify themselves as browsers
private static List<String> getBrowserIDs(PackageManager pm) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.mozilla.org/"));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// We've tried using PackageManager.MATCH_DEFAULT_ONLY flag and found that browsers that
// are not set as the default browser won't be matched even if they had CATEGORY_DEFAULT set
// in the intent filter
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
List<String> browsers = new ArrayList<String>();
for (int i = 0; i < resolveInfos.size(); i++) {
ResolveInfo info = resolveInfos.get(i);
String browserID = info.activityInfo.packageName;
browsers.add(browserID);
}
return browsers;
}
// Gets called in AndroidAuthenticationListener;
public static boolean isWebViewSupported(Context ctx) {
Log.v(TAG, "Checking if installed Webview is compatible with FxA");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// The default Webview is able do to FXA
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PackageInfo pi = WebView.getCurrentWebViewPackage();
if (CHROME_BROWSERS.contains(pi.packageName)) {
return isSupportedChromeBrowser(pi);
}
return isNotAncientBrowser(pi);
}
// Before O the webview is hardcoded, but we dont know which package it is.
// Check if com.google.android.webview is installed
PackageManager pm = ctx.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo("com.google.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
// Otherwise check com.android.webview
try {
PackageInfo pi = pm.getPackageInfo("com.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
Log.e(TAG, "Android System WebView is not found");
// Giving up :(
return false;
}
private static boolean isSupportedChromeBrowser(PackageInfo pi) {
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
try {
String versionCode = pi.versionName.split(Pattern.quote(" "))[0];
String majorVersion = versionCode.split(Pattern.quote("."))[0];
int version = Integer.parseInt(majorVersion);
return version >= MIN_CHROME_VERSION;
} catch (Exception e) {
Log.e(TAG, "Failed to check Chrome Version Code " + pi.versionName);
return false;
}
}
private static boolean isNotAncientBrowser(PackageInfo pi) {
// Not a google chrome - So the version name is worthless
// Lets just make sure the WebView
// used is not ancient ==> Was updated in at least the last 365 days
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
double oneYearInMillis = 31536000000L;
return pi.lastUpdateTime > (System.currentTimeMillis() - oneYearInMillis);
}
}

View file

@ -0,0 +1,33 @@
/* 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.amnezia.vpn.qt;
import android.view.KeyEvent;
public class VPNActivity extends org.qtproject.qt5.android.bindings.QtActivity {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
onBackPressed();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
try {
if (!handleBackButton()) {
// Move the activity into paused state if back button was pressed
moveTaskToBack(true);
}
} catch (Exception e) {
}
}
// Returns true if MVPN has handled the back button
native boolean handleBackButton();
}

View file

@ -0,0 +1,18 @@
package org.amnezia.vpn.qt;
import android.app.Activity;
import android.os.Bundle;
import org.amnezia.vpn.BuildConfig;
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
private static VPNApplication instance;
@Override
public void onCreate() {
super.onCreate();
VPNApplication.instance = this;
}
}

View file

@ -0,0 +1,37 @@
/* 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.amnezia.vpn.qt
import android.content.Context
import android.content.Intent
class VPNPermissionHelper : android.net.VpnService() {
/**
* This small service does nothing else then checking if the vpn permission
* is present and prompting if not.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val intent = prepare(this.applicationContext)
if (intent != null) {
startActivityForResult(intent)
}
return START_NOT_STICKY
}
companion object {
@JvmStatic
fun startService(c: Context) {
val appC = c.applicationContext
appC.startService(Intent(appC, VPNPermissionHelper::class.java))
}
}
/**
* Fetches the Global QTAndroidActivity and calls startActivityForResult with the given intent
* Is used to request the VPN-Permission, if not given.
* Actually Implemented in src/platforms/android/AndroidController.cpp
*/
external fun startActivityForResult(i: Intent)
}