Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ build/
*.iml

# Compiled JNI libraries folder
**/jniLibs
**/jniLibs/*
!app/src/main/jniLibs/x86_64/
!app/src/main/jniLibs/x86_64/libeasytier_android_jni.so
!app/src/main/jniLibs/x86_64/libeasytier_ffi.so
!app/src/main/jniLibs/x86_64/README.md
app/.externalNativeBuild/

# NDK stuff
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def escapeBuildConfigString = { value ->
value.replace("\\", "\\\\").replace("\"", "\\\"")
}
def enableX86TestAbi = (project.findProperty("enableX86TestAbi") ?: "false").toString().toBoolean()
def enableX8664TestAbi = (project.findProperty("enableX8664TestAbi") ?: "false").toString().toBoolean()
def audioHapticsAppLabel = "Moonlight V+"
def audioHapticsSdkDir = (
project.findProperty("audioHapticsSdkDir")
Expand Down Expand Up @@ -81,6 +82,9 @@ android {
if (enableX86TestAbi) {
supportedAbis += 'x86'
}
if (enableX8664TestAbi) {
supportedAbis += 'x86_64'
}
abiFilters(*supportedAbis)
}
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
<activity
android:name=".PcView"
android:exported="true"
android:launchMode="singleTop"
android:resizeableActivity="true"
android:banner="@drawable/atv_banner"
android:icon="@mipmap/ic_launcher_saba"
Expand All @@ -200,6 +201,16 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="moonlight"
android:host="pair" />
</intent-filter>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</activity>
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/easytier/jni/EasyTierJNI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ package com.easytier.jni
object EasyTierJNI {

init {
// 加载本地库 (libeasytier_android_jni.so)
// The JNI bridge calls symbols exported by libeasytier_ffi.so. Load the
// dependency explicitly as well as retaining the ELF DT_NEEDED link used
// by release builds, so test ABIs and vendor-packaged builds initialize in
// the same deterministic order.
System.loadLibrary("easytier_ffi")
System.loadLibrary("easytier_android_jni")
}

Expand Down
28 changes: 22 additions & 6 deletions app/src/main/java/com/easytier/jni/EasyTierManager.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.easytier.jni

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
Expand All @@ -11,10 +11,11 @@ import org.json.JSONObject
import java.util.Objects

class EasyTierManager(
private val activity: Activity,
context: Context,
private val instanceName: String,
private val networkConfig: String
internal val networkConfig: String
) {
private val appContext = context.applicationContext
private val handler = Handler(Looper.getMainLooper())

@Volatile
Expand Down Expand Up @@ -177,17 +178,18 @@ class EasyTierManager(
}

private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
val intent = Intent(activity, EasyTierVpnService::class.java)
val intent = Intent(appContext, EasyTierVpnService::class.java)
intent.putExtra("ipv4_address", ipv4)
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
intent.putExtra("instance_name", instanceName)
activity.startService(intent)
appContext.startService(intent)
vpnServiceIntent = intent
}

private fun stopVpnService() {
val stopIntent = Intent(EasyTierVpnService.ACTION_STOP_VPN)
activity.sendBroadcast(stopIntent)
stopIntent.setPackage(appContext.packageName)
appContext.sendBroadcast(stopIntent)
Log.i(TAG, "停止发送VPN广播。")
vpnServiceIntent = null
}
Expand Down Expand Up @@ -218,3 +220,17 @@ class EasyTierManager(
private const val MONITOR_INTERVAL = 3000L
}
}

/** Keeps the active mesh alive across activity recreation and streaming UI transitions. */
object EasyTierRuntime {
@Volatile
private var manager: EasyTierManager? = null

@Synchronized
fun getOrCreate(context: Context, instanceName: String, networkConfig: String): EasyTierManager {
val current = manager
if (current != null && current.networkConfig == networkConfig) return current
current?.stop()
return EasyTierManager(context, instanceName, networkConfig).also { manager = it }
}
}
14 changes: 13 additions & 1 deletion app/src/main/java/com/easytier/jni/EasyTierVpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class EasyTierVpnService : VpnService() {
val builder = Builder()
builder.setSession("EasyTier VPN")
.addAddress(addressInfo.ip, addressInfo.networkLength)
.addDnsServer("223.5.5.5")
.addRoute(networkAddress(addressInfo.ip, addressInfo.networkLength), addressInfo.networkLength)

try {
builder.addAddress("fd00::1", 128)
Expand Down Expand Up @@ -162,6 +162,18 @@ class EasyTierVpnService : VpnService() {
return IpAddressInfo(parts[0], parts[1].toInt())
}

private fun networkAddress(ip: String, prefixLength: Int): String {
require(prefixLength in 0..32) { "Invalid IPv4 prefix length" }
val value = ip.split('.').fold(0L) { result, octet ->
val parsed = octet.toInt()
require(parsed in 0..255) { "Invalid IPv4 address" }
(result shl 8) or parsed.toLong()
}
val mask = if (prefixLength == 0) 0L else (0xffffffffL shl (32 - prefixLength)) and 0xffffffffL
val network = value and mask
return "${(network shr 24) and 0xff}.${(network shr 16) and 0xff}.${(network shr 8) and 0xff}.${network and 0xff}"
}

companion object {
private const val TAG = "EasyTierVpnService"
const val ACTION_STOP_VPN = "com.easytier.jni.ACTION_STOP_VPN"
Expand Down
92 changes: 78 additions & 14 deletions app/src/main/java/com/limelight/PcView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.StringReader
import java.net.InetSocketAddress
import java.net.Socket
import java.net.UnknownHostException
import java.text.SimpleDateFormat
import java.util.Date
Expand Down Expand Up @@ -63,6 +65,8 @@ import com.limelight.utils.CacheHelper
import com.limelight.utils.ConfigurationSyncScheduler
import com.limelight.utils.Dialog
import com.limelight.utils.easytier.EasyTierController
import com.limelight.utils.easytier.VPlusConnectionCode
import com.limelight.utils.easytier.VPlusConnectionCodeParser
import com.limelight.utils.HelpLauncher
import com.limelight.utils.Iperf3Tester
import com.limelight.utils.NetHelper
Expand Down Expand Up @@ -213,6 +217,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
private const val NETWORK_QUALITY_ID = 18
private const val ADD_PC_MANUALLY_ID = 19
private const val ADD_PC_QR_SCAN_ID = 20
private const val REMOTE_HOST_CONNECT_TIMEOUT_MS = 30_000L

}

Expand All @@ -236,6 +241,8 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
private var lastShakeTime = 0L
private var activeSceneNumber: Int? = null
private var pendingAddedComputerUuid: String? = null
private var pendingRemoteConnectionCode: VPlusConnectionCode? = null
private var pendingConnectionIntentUrl: String? = null
private val exitGate = PcViewExitGate()

// Helpers
Expand Down Expand Up @@ -283,6 +290,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
// 与网络往返重叠,最坏情况持平、最佳情况节省整段证书时间。
managerBinder = localBinder
showPendingAddedComputer()
consumePendingConnectionIntent()
startComputerUpdates()

// 后台预热:等 DiscoveryService bind(mDNS 可能还没好),并把客户端证书
Expand Down Expand Up @@ -370,6 +378,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}

easyTierController = EasyTierController(this, this)
captureConnectionIntent(intent)
inForeground = true

val glPrefs = GlPreferences.readPreferences(this)
Expand All @@ -389,6 +398,12 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
AboutDialogLauncher.onConfigurationChanged(this, newConfig)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
captureConnectionIntent(intent)
}

override fun onResume() {
super.onResume()
UiHelper.showDecoderCrashDialog(this)
Expand Down Expand Up @@ -2119,29 +2134,51 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
integrator.initiateScan()
}

private fun handleQrPairResult(url: String) {
val uri = url.toUri()
if ("moonlight" != uri.scheme || "pair" != uri.host) {
showToast(getString(R.string.qr_invalid_code))
private fun captureConnectionIntent(intent: Intent?) {
val uri = intent?.data ?: return
if (intent.action != Intent.ACTION_VIEW ||
!uri.scheme.equals("moonlight", ignoreCase = true) ||
!uri.host.equals("pair", ignoreCase = true)
) {
return
}

val host = uri.getQueryParameter("host")
val portStr = uri.getQueryParameter("port")
val pin = uri.getQueryParameter("pin")
pendingConnectionIntentUrl = uri.toString()
consumePendingConnectionIntent()
}

private fun consumePendingConnectionIntent() {
if (!completeOnCreateCalled || managerBinder == null || easyTierController == null) return
val url = pendingConnectionIntentUrl ?: return
pendingConnectionIntentUrl = null
handleQrPairResult(url)
}

if (host == null || pin == null) {
private fun handleQrPairResult(url: String) {
val code = try {
VPlusConnectionCodeParser.parse(url)
} catch (_: IllegalArgumentException) {
showToast(getString(R.string.qr_invalid_code))
return
}

var port = NvHTTP.DEFAULT_HTTP_PORT
if (portStr != null) {
try { port = portStr.toInt() } catch (ignored: NumberFormatException) {}
if (code.easyTierProfile != null) {
pendingRemoteConnectionCode = code
showToast(getString(R.string.remote_connect_preparing))
try {
easyTierController?.activateConnectionProfile(code.easyTierProfile)
?: throw IllegalStateException("EasyTier controller unavailable")
} catch (_: Exception) {
pendingRemoteConnectionCode = null
showToast(getString(R.string.remote_connect_setup_failed))
}
} else {
pairFromConnectionCode(code, waitForRemoteHost = false)
}
}

private fun pairFromConnectionCode(code: VPlusConnectionCode, waitForRemoteHost: Boolean) {
showToast(getString(R.string.qr_pairing))
val finalPort = port
uiScope.launch {
var message: String?
var success = false
Expand All @@ -2150,10 +2187,16 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
try {
stopComputerUpdatesAndWait()

if (waitForRemoteHost && !waitForHostEndpoint(code.host, code.port)) {
showToast(getString(R.string.remote_connect_host_timeout))
startComputerUpdates()
return@launch
}

val result = withContext(Dispatchers.IO) {
// Add the computer first
val addDetails = ComputerDetails()
addDetails.manualAddress = ComputerDetails.AddressTuple(host, finalPort)
addDetails.manualAddress = ComputerDetails.AddressTuple(code.host, code.port)
val added = managerBinder?.addComputerBlocking(addDetails) == true
if (!added) {
return@withContext QrPairResult(getString(R.string.addpc_fail), false, null, null)
Expand All @@ -2179,7 +2222,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}

val pm = httpConn.pairingManager
val pairResult = pm.pair(httpConn.getServerInfo(true), pin)
val pairResult = pm.pair(httpConn.getServerInfo(true), code.pin)
when (pairResult.state) {
PairState.PIN_WRONG ->
QrPairResult(getString(R.string.pair_incorrect_pin), false, null, null)
Expand Down Expand Up @@ -2227,6 +2270,22 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}
}

private suspend fun waitForHostEndpoint(host: String, port: Int): Boolean =
withContext(Dispatchers.IO) {
val deadline = SystemClock.elapsedRealtime() + REMOTE_HOST_CONNECT_TIMEOUT_MS
while (SystemClock.elapsedRealtime() < deadline) {
try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 1_000)
return@withContext true
}
} catch (_: IOException) {
Thread.sleep(500)
}
}
false
}

private data class QrPairResult(
val message: String?,
val success: Boolean,
Expand Down Expand Up @@ -3353,6 +3412,11 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_PERMISSION_REQUEST_CODE && easyTierController != null) {
easyTierController?.handleVpnPermissionResult(resultCode)
val pendingCode = pendingRemoteConnectionCode
pendingRemoteConnectionCode = null
if (resultCode == RESULT_OK && pendingCode != null) {
pairFromConnectionCode(pendingCode, waitForRemoteHost = true)
}
} else if (requestCode == UpdateManager.INSTALL_PERMISSION_REQUEST_CODE) {
UpdateManager.onInstallPermissionResult(this)
}
Expand Down
Loading
Loading