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
42 changes: 42 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Android CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Build with Gradle
run: ./gradlew assembleFdroid_freeDebug

- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: swiftp-debug-apk
path: app/build/outputs/apk/fdroid_free/debug/*.apk
7 changes: 6 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.twofortyfouram:android-plugin-client-sdk-for-locale:4.0.3'
implementation ('com.twofortyfouram:android-plugin-client-sdk-for-locale:4.0.3') {
exclude group: 'com.twofortyfouram', module: 'android-annotation'
exclude group: 'com.twofortyfouram', module: 'android-assertion'
}
implementation 'com.twofortyfouram:android-annotation:3.0.0'
implementation 'com.twofortyfouram:android-assertion:2.0.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.jetbrains:annotations:23.0.0'
implementation 'androidx.work:work-runtime:2.9.0'
Expand Down
114 changes: 106 additions & 8 deletions app/src/main/java/be/ppareit/swiftp/FsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -448,17 +448,23 @@ public static InetAddress getLocalInetAddress() {
ArrayList<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface networkInterface : networkInterfaces) {
// only check network interfaces that give local connection
if (!networkInterface.getName().matches("^(eth|wlan|tun).*"))
// Include ap0 for WiFi hotspot and rndis for USB tethering
if (!networkInterface.getName().matches("^(eth|wlan|ap|rndis|tun).*"))
continue;
for (InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
// Accept site-local addresses (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
// Also accept private addresses even if not classified as site-local
if (!address.isLoopbackAddress()
&& !address.isLinkLocalAddress()
&& address.isSiteLocalAddress()
&& address instanceof Inet4Address) {
if (returnAddress != null) {
Cat.w("Found more than one valid address local inet address, why???");
String addressStr = address.getHostAddress();
// Check if it's a private IP range (RFC 1918)
if (isPrivateIpAddress(addressStr) || address.isSiteLocalAddress()) {
if (returnAddress != null) {
Cat.w("Found more than one valid address local inet address, why???");
}
returnAddress = address;
}
returnAddress = address;
}
}
}
Expand All @@ -468,6 +474,34 @@ public static InetAddress getLocalInetAddress() {
return returnAddress;
}

/**
* Check if an IP address is in a private range (RFC 1918)
* @param ipAddress IP address as string
* @return true if address is in private range
*/
private static boolean isPrivateIpAddress(String ipAddress) {
try {
String[] parts = ipAddress.split("\\.");
if (parts.length != 4) return false;

int firstOctet = Integer.parseInt(parts[0]);
int secondOctet = Integer.parseInt(parts[1]);

// 10.0.0.0 - 10.255.255.255
if (firstOctet == 10) return true;

// 172.16.0.0 - 172.31.255.255
if (firstOctet == 172 && secondOctet >= 16 && secondOctet <= 31) return true;

// 192.168.0.0 - 192.168.255.255
if (firstOctet == 192 && secondOctet == 168) return true;

return false;
} catch (Exception e) {
return false;
}
}

/**
* Checks to see if we are connected to a local network, for instance wifi or ethernet
*
Expand All @@ -483,25 +517,89 @@ public static boolean isConnectedToLocalNetwork() {
Log.d(TAG, "isConnectedToLocalNetwork: see if it is an WIFI AP");
WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
try {
// Try the reflection method first (works on older Android versions)
Method method = wm.getClass().getDeclaredMethod("isWifiApEnabled");
connected = (Boolean) method.invoke(wm);
} catch (Exception e) {
e.printStackTrace();
Log.v(TAG, "isWifiApEnabled reflection failed, will check network interfaces");
}
}
if (!connected) {
// Fallback: check for WiFi hotspot by looking for ap0 interface
Log.d(TAG, "isConnectedToLocalNetwork: checking for WiFi hotspot interface");
try {
ArrayList<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface netInterface : networkInterfaces) {
String ifname = netInterface.getName();
// ap0 is used for WiFi hotspot, check if it has valid IPv4 addresses
if (ifname.startsWith("ap")) {
ArrayList<InetAddress> addresses = Collections.list(netInterface.getInetAddresses());
for (InetAddress addr : addresses) {
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
connected = true;
Log.d(TAG, "Found WiFi hotspot interface: " + ifname);
break;
}
}
if (connected) break;
}
}
} catch (SocketException e) {
Log.v(TAG, "Exception checking for ap0 interface: " + e);
}
}
if (!connected) {
Log.d(TAG, "isConnectedToLocalNetwork: see if it is an USB AP");
try {
ArrayList<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface netInterface : networkInterfaces) {
if (netInterface.getDisplayName().startsWith("rndis")) {
connected = true;
String ifname = netInterface.getName();
// rndis is used for USB tethering
if (ifname.startsWith("rndis")) {
ArrayList<InetAddress> addresses = Collections.list(netInterface.getInetAddresses());
for (InetAddress addr : addresses) {
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
connected = true;
Log.d(TAG, "Found USB tethering interface: " + ifname);
break;
}
}
if (connected) break;
}
}
} catch (SocketException e) {
e.printStackTrace();
}
}
if (!connected) {
// Final fallback: check if any non-loopback interfaces have valid local addresses
Log.d(TAG, "isConnectedToLocalNetwork: checking all network interfaces for local addresses");
try {
ArrayList<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface netInterface : networkInterfaces) {
String ifname = netInterface.getName();
// Skip loopback and certain system interfaces
if (ifname.startsWith("lo") || ifname.startsWith("dummy")) {
continue;
}
ArrayList<InetAddress> addresses = Collections.list(netInterface.getInetAddresses());
for (InetAddress addr : addresses) {
if (addr instanceof Inet4Address && !addr.isLoopbackAddress() && !addr.isLinkLocalAddress()) {
String addressStr = addr.getHostAddress();
// Check if it's a private IP
if (isPrivateIpAddress(addressStr)) {
connected = true;
Log.d(TAG, "Found private IP on interface " + ifname + ": " + addressStr);
break;
}
}
}
if (connected) break;
}
} catch (SocketException e) {
Log.v(TAG, "Exception during final network interface check: " + e);
}
}
return connected;
}

Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/menu/logs_menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

<item
android:id="@+id/action_display_logcat"
android:checkable="true"
android:title="@string/display_logcat" />
<item
android:id="@+id/action_clear"
android:title="@string/clear" />

</menu>
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

buildscript {
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
Expand All @@ -15,7 +15,7 @@ buildscript {

allprojects {
repositories {
jcenter()
mavenCentral()
google()
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
org.gradle.unsafe.configuration-cache=true
org.gradle.unsafe.configuration-cache=false
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false