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
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;

Expand All @@ -13,6 +16,7 @@
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.InetAddress;
import java.security.Security;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -137,6 +141,120 @@ public static boolean isNetworkAvailable() {
return networkInfo != null && networkInfo.isConnectedOrConnecting();
}

public static boolean isLocalNetworkUrl(String urlString) {
if (urlString == null || urlString.isEmpty()) return false;
try {
Uri uri = Uri.parse(urlString);
String host = uri.getHost();
if (host == null) return false;

host = host.toLowerCase();

if (host.equals("localhost") || host.equals("127.0.0.1") || host.equals("::1")) {
return true;
}

// Common local domain suffixes
if (host.endsWith(".local") || host.endsWith(".lan") || host.endsWith(".home")
|| host.endsWith(".internal") || host.endsWith(".fritz.box")) {
return true;
}

// Check if it's a numeric IP address and if it's in private ranges
if (isPrivateIPv4(host)) {
return true;
}

// IPv6 unique local address (fc00::/7) or link-local (fe80::/10)
if (host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80")) {
return true;
}

} catch (Exception e) {
Log.e(TAG, "isLocalNetworkUrl() error parsing URL: " + urlString, e);
}
return false;
}

public interface LocalNetworkCheckCallback {
void onResult(boolean isLocal);
}

public static void checkIsLocalNetworkUrl(final String urlString, final LocalNetworkCheckCallback callback) {
if (urlString == null || urlString.isEmpty() || urlString.equals("https://") || urlString.equals("http://")) {
callback.onResult(false);
return;
}

// Fast path for string-based check
if (isLocalNetworkUrl(urlString)) {
callback.onResult(true);
return;
}

try {
Uri uri = Uri.parse(urlString);
String host = uri.getHost();
if (host == null || host.isEmpty()) {
callback.onResult(false);
return;
}
} catch (Exception e) {
callback.onResult(false);
return;
}

// Slow path: DNS resolution
new Thread(new Runnable() {
@Override
public void run() {
boolean isLocal = false;
try {
Uri uri = Uri.parse(urlString);
String host = uri.getHost();
if (host != null) {
InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress addr : addresses) {
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
isLocal = true;
break;
}
}
}
} catch (Exception e) {
Log.w(TAG, "checkIsLocalNetworkUrl() DNS resolution failed for " + urlString);
}

final boolean result = isLocal;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(result);
}
});
}
}).start();
}

private static boolean isPrivateIPv4(String host) {
// Simple regex for IPv4
String ipv4Pattern = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$";
if (host.matches(ipv4Pattern)) {
try {
String[] parts = host.split("\\.");
int first = Integer.parseInt(parts[0]);
int second = Integer.parseInt(parts[1]);
if (first == 10) return true;
if (first == 172 && second >= 16 && second <= 31) return true;
if (first == 192 && second == 168) return true;
if (first == 169 && second == 254) return true; // link-local
} catch (NumberFormatException e) {
return false;
}
}
return false;
}

public static OkHttpClient createClient() {
return createClient(true);
}
Expand Down
30 changes: 28 additions & 2 deletions app/src/main/java/fr/gaulupeau/apps/Poche/ui/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package fr.gaulupeau.apps.Poche.ui;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.XmlResourceParser;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
Expand Down Expand Up @@ -86,6 +89,7 @@ public class MainActivity extends AppCompatActivity
private static final String FRAGMENT_ARTICLE_LISTS = "fragment_article_lists";
private static final String FRAGMENT_TAG_LIST = "fragment_tag_list";
private static final String FRAGMENT_TAGGED_ARTICLE_LISTS = "fragment_tagged_article_lists";
private static final int REQUEST_LOCAL_NETWORK = 1001;

private Settings settings;

Expand Down Expand Up @@ -128,6 +132,8 @@ protected void onCreate(Bundle savedInstanceState) {

setContentView(R.layout.activity_main);

settings = App.getSettings();

WindowInsetsControllerCompat windowInsetsController =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
windowInsetsController.setAppearanceLightStatusBars(Themes.getCurrentTheme() == Themes.Theme.LIGHT
Expand All @@ -136,8 +142,6 @@ protected void onCreate(Bundle savedInstanceState) {

setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);

settings = App.getSettings();

getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
Expand Down Expand Up @@ -332,6 +336,25 @@ protected void onStart() {
tryToUpdateOnResume = true;
}

private void requestLocalNetworkPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { // API 37
if (checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) != PackageManager.PERMISSION_GRANTED) {
if (settings != null && settings.isConfigurationOk() && settings.getUrl() != null) {
WallabagConnection.checkIsLocalNetworkUrl(settings.getUrl(), isLocal -> {
if (isLocal) {
if (checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new String[]{Manifest.permission.ACCESS_LOCAL_NETWORK},
REQUEST_LOCAL_NETWORK
);
}
}
});
}
}
}
}

@Override
protected void onResume() {
super.onResume();
Expand Down Expand Up @@ -360,6 +383,8 @@ protected void onResume() {
}
}

requestLocalNetworkPermissionIfNeeded();

if (tryToUpdateOnResume) {
tryToUpdateOnResume = false;

Expand Down Expand Up @@ -931,6 +956,7 @@ private void testConfiguration() {
this, new ConfigurationTestHelper.ResultHandler() {
@Override
public void onConfigurationTestSuccess(String url) {
requestLocalNetworkPermissionIfNeeded();
updateAllFeedsIfDbIsEmpty();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package fr.gaulupeau.apps.Poche.ui.preferences;

import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
Expand Down Expand Up @@ -44,6 +47,7 @@ public class ConnectionWizardActivity extends BaseActionBarActivity {
private static final String TAG = "ConnectionWizard";

private static final int REQUEST_CODE_QR_CODE = 1;
private static final int REQUEST_LOCAL_NETWORK = 1001;

private static final String DATA_PROVIDER = "provider";
private static final String DATA_URL = "url";
Expand Down Expand Up @@ -191,6 +195,28 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}

private void requestLocalNetworkPermissionIfNeeded(String url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) {
return;
}

if (checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) == PackageManager.PERMISSION_GRANTED) {
return;
}

if (TextUtils.isEmpty(url)) {
return;
}

WallabagConnection.checkIsLocalNetworkUrl(url, isLocal -> {
if (isLocal
&& checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.ACCESS_LOCAL_NETWORK},
REQUEST_LOCAL_NETWORK);
}
});
}

private ConnectionData parseLoginData(String connectionUri) {
// wallabag://user@server.tld
String prefix = "wallabag://";
Expand Down Expand Up @@ -512,6 +538,10 @@ protected void gatherData() {
protected void runTest() {
cancelTest();

if (activity != null) {
activity.requestLocalNetworkPermissionIfNeeded(url);
}

configurationTestHelper = new ConfigurationTestHelper(
activity, this, this, url, httpAuthUsername, httpAuthPassword,
username, password, clientID, clientSecret, tryPossibleURLs, false);
Expand Down