[network/kdeconnect-android] /: Target Android 17

Albert Vaca Cintora <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3acdcefdb74182ebfeabf710ca1cb0b454fb6d0f by Albert Vaca Cintora.
Committed on 30/07/2026 at 19:46.
Pushed by albertvaka into branch 'master'.

Target Android 17

Starting in API 37 (Android 17), a new ACCESS_LOCAL_NETWORK permission is
needed to communicate on the local network. Request this on app start and
as a banner if it's missing, similar to what we did for the notifications
permission since Android 13.

M  +1    -1    build.gradle.kts
M  +1    -0    src/main/AndroidManifest.xml
M  +8    -0    src/main/java/org/kde/kdeconnect/backends/lan/LanLinkProvider.java
M  +13   -0    src/main/java/org/kde/kdeconnect/backends/lan/MdnsDiscovery.kt
M  +18   -9    src/main/java/org/kde/kdeconnect/ui/MainActivity.kt
M  +26   -8    src/main/java/org/kde/kdeconnect/ui/PairingFragment.kt
M  +28   -7    src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingScreen.kt
M  +1    -0    src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingUiState.kt
M  +3    -0    src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingViewModel.kt
M  +1    -0    src/main/res/values/strings.xml

https://invent.kde.org/network/kdeconnect-android/-/commit/3acdcefdb74182ebfeabf710ca1cb0b454fb6d0f

diff --git a/build.gradle.kts b/build.gradle.kts
index 6f2d83307..806e777e1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -40,7 +40,7 @@ android {
     defaultConfig {
         applicationId = "org.kde.kdeconnect_tp"
         minSdk = 23
-        targetSdk = 35
+        targetSdk = 37
         versionCode = 13509
         versionName = "1.35.9"
         proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index e1956e584..570376cb9 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -35,6 +35,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
 
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
     <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
diff --git a/src/main/java/org/kde/kdeconnect/backends/lan/LanLinkProvider.java b/src/main/java/org/kde/kdeconnect/backends/lan/LanLinkProvider.java
index 486d486c7..5fbfe366a 100644
--- a/src/main/java/org/kde/kdeconnect/backends/lan/LanLinkProvider.java
+++ b/src/main/java/org/kde/kdeconnect/backends/lan/LanLinkProvider.java
@@ -6,16 +6,20 @@
 
 package org.kde.kdeconnect.backends.lan;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static org.kde.kdeconnect.helpers.NetworkHelperKt.isPrivateAddress;
 import static main.java.org.kde.kdeconnect.helpers.BoundedLineReaderKt.readLineBounded;
 
+import android.Manifest;
 import android.content.Context;
 import android.net.Network;
+import android.os.Build;
 import android.util.Log;
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
+import androidx.core.content.ContextCompat;
 
 import org.json.JSONException;
 import org.kde.kdeconnect.backends.BaseLink;
@@ -486,6 +490,10 @@ public class LanLinkProvider extends BaseLinkProvider {
     }
 
     private void broadcastUdpIdentityPacket(@Nullable Network network) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) != PERMISSION_GRANTED) {
+            Log.w("LanLinkProvider", "Will not UDP broadcast, missing ACCESS_LOCAL_NETWORK permission");
+            return;
+        }
         ThreadHelper.execute(() -> {
             List<DeviceHost> hostList = CustomDevicesActivity
                     .getCustomDeviceList(context);
diff --git a/src/main/java/org/kde/kdeconnect/backends/lan/MdnsDiscovery.kt b/src/main/java/org/kde/kdeconnect/backends/lan/MdnsDiscovery.kt
index 969f91cfb..817b7bfbb 100644
--- a/src/main/java/org/kde/kdeconnect/backends/lan/MdnsDiscovery.kt
+++ b/src/main/java/org/kde/kdeconnect/backends/lan/MdnsDiscovery.kt
@@ -5,14 +5,19 @@
  */
 package org.kde.kdeconnect.backends.lan
 
+import android.Manifest
 import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.net.nsd.NsdManager
 import android.net.nsd.NsdManager.DiscoveryListener
 import android.net.nsd.NsdManager.RegistrationListener
 import android.net.nsd.NsdServiceInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.MulticastLock
+import android.os.Build
 import android.util.Log
+import androidx.core.content.ContextCompat
 import org.kde.kdeconnect.helpers.DeviceHelper
 import org.kde.kdeconnect.helpers.DeviceHelper.deviceType
 import org.kde.kdeconnect.helpers.DeviceHelper.getDeviceId
@@ -38,6 +43,10 @@ class MdnsDiscovery {
     }
 
     fun startDiscovering() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) != PERMISSION_GRANTED) {
+            Log.w("MdnsDiscover", "Will not MDNS discover, missing ACCESS_LOCAL_NETWORK permission")
+            return
+        }
         if (discoveryListener == null) {
             multicastLock.acquire()
             discoveryListener = createDiscoveryListener()
@@ -58,6 +67,10 @@ class MdnsDiscovery {
     }
 
     fun startAnnouncing() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) != PERMISSION_GRANTED) {
+            Log.w("MdnsDiscover", "Will not MDNS announce, missing ACCESS_LOCAL_NETWORK permission")
+            return
+        }
         if (registrationListener == null) {
             val serviceInfo: NsdServiceInfo?
             try {
diff --git a/src/main/java/org/kde/kdeconnect/ui/MainActivity.kt b/src/main/java/org/kde/kdeconnect/ui/MainActivity.kt
index 06907c97a..dc7439262 100644
--- a/src/main/java/org/kde/kdeconnect/ui/MainActivity.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/MainActivity.kt
@@ -11,6 +11,7 @@ import android.content.Intent
 import android.content.SharedPreferences
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.os.Build
 import android.os.Bundle
 import android.util.Log
@@ -218,16 +219,21 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
 
         val missingPermissions = mutableListOf<String>()
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            val permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
-            if (permissionResult != PackageManager.PERMISSION_GRANTED) {
-                if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
-                    missingPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
-                }
-            }
+        // Upfront requests for permissions without explanation
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
+            && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_LOCAL_NETWORK) != PERMISSION_GRANTED
+            && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_LOCAL_NETWORK))
+        {
+            missingPermissions.add(Manifest.permission.ACCESS_LOCAL_NETWORK)
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+            && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED
+            && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS))
+        {
+            missingPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
         }
 
-        if(missingPermissions.isNotEmpty()){
+        if (missingPermissions.isNotEmpty()) {
             ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), RESULT_NOTIFICATIONS_ENABLED)
         }
     }
@@ -392,7 +398,10 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
                 setContentFragment(SettingsFragment())
             }
 
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isPermissionGranted(permissions, grantResults, Manifest.permission.POST_NOTIFICATIONS)) {
+            if (
+                (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isPermissionGranted(permissions, grantResults, Manifest.permission.POST_NOTIFICATIONS))
+                || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && isPermissionGranted(permissions, grantResults, Manifest.permission.ACCESS_LOCAL_NETWORK))
+            ) {
                 // If PairingFragment is active, reload it
                 if (mCurrentDevice == null) {
                     setContentFragment(PairingFragment())
diff --git a/src/main/java/org/kde/kdeconnect/ui/PairingFragment.kt b/src/main/java/org/kde/kdeconnect/ui/PairingFragment.kt
index b5fd6c5a8..777b34ffd 100644
--- a/src/main/java/org/kde/kdeconnect/ui/PairingFragment.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/PairingFragment.kt
@@ -21,6 +21,7 @@ import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
 import androidx.compose.runtime.getValue
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
@@ -94,18 +95,18 @@ class PairingFragment : BaseFragment<DevicesListBinding>() {
         createComposeView()
     }
 
-    fun hasRequestedNotificationPermission(): Boolean {
+    fun hasRequestedPermission(permission: String): Boolean {
         return context
             ?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
-            ?.getBoolean(KEY_REQUESTED_NOTIFICATIONS, false)
+            ?.getBoolean(permission, false)
             ?: false
     }
 
-    fun markNotificationPermissionRequested() {
+    fun markPermissionRequested(permission: String) {
         context
             ?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
             ?.edit {
-                putBoolean(KEY_REQUESTED_NOTIFICATIONS, true)
+                putBoolean(permission, true)
             }
     }
 
@@ -127,12 +128,12 @@ class PairingFragment : BaseFragment<DevicesListBinding>() {
                         },
                         onNotificationSettingsClick = {
                             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                                val hasRequestedBefore = hasRequestedNotificationPermission()
-                                val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(mActivity!!, Manifest.permission.POST_NOTIFICATIONS)
+                                val hasRequestedBefore = hasRequestedPermission(Manifest.permission.POST_NOTIFICATIONS)
+                                val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.POST_NOTIFICATIONS)
                                 if (hasRequestedBefore && !shouldShowRationale) {
                                     openAppDetailsSettings()
                                 } else {
-                                    markNotificationPermissionRequested()
+                                    markPermissionRequested(Manifest.permission.POST_NOTIFICATIONS)
                                     ActivityCompat.requestPermissions(
                                         requireActivity(),
                                         arrayOf(Manifest.permission.POST_NOTIFICATIONS),
@@ -143,6 +144,20 @@ class PairingFragment : BaseFragment<DevicesListBinding>() {
                                 openAppDetailsSettings()
                             }
                         },
+                        onLocalNetworkPermissionClick = @RequiresApi(Build.VERSION_CODES.CINNAMON_BUN) {
+                            val hasRequestedBefore = hasRequestedPermission(Manifest.permission.ACCESS_LOCAL_NETWORK)
+                            val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.ACCESS_LOCAL_NETWORK)
+                            if (hasRequestedBefore && !shouldShowRationale) {
+                                openAppDetailsSettings()
+                            } else {
+                                markPermissionRequested(Manifest.permission.ACCESS_LOCAL_NETWORK)
+                                ActivityCompat.requestPermissions(
+                                    requireActivity(),
+                                    arrayOf(Manifest.permission.ACCESS_LOCAL_NETWORK),
+                                    MainActivity.RESULT_NOTIFICATIONS_ENABLED
+                                )
+                            }
+                        },
                         onRefresh = { viewModel.onRefresh() }
                     )
                 }
@@ -215,8 +230,12 @@ class PairingFragment : BaseFragment<DevicesListBinding>() {
         val hasNotificationsPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
                 || ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) == PERMISSION_GRANTED
 
+        val hasLocalNetworkPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.CINNAMON_BUN
+                || ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED
+
         viewModel.updatePermissions(
             hasNotificationsPermission = hasNotificationsPermission,
+            hasLocalNetworkPermission = hasLocalNetworkPermission,
         )
     }
 
@@ -256,6 +275,5 @@ class PairingFragment : BaseFragment<DevicesListBinding>() {
         private const val RESULT_PAIRING_SUCCESFUL = Activity.RESULT_FIRST_USER
 
         private const val PREFS_NAME = "permission_prefs"
-        private const val KEY_REQUESTED_NOTIFICATIONS = "requested_notifications_permission"
     }
 }
diff --git a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingScreen.kt b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingScreen.kt
index 2e67ff73d..3e7c8400e 100644
--- a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingScreen.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingScreen.kt
@@ -58,6 +58,7 @@ fun PairingScreen(
     onClick: (String) -> Unit,
     onWifiSettingsClick: () -> Unit = {},
     onNotificationSettingsClick: () -> Unit = {},
+    onLocalNetworkPermissionClick: () -> Unit = {},
     onRefresh: () -> Unit = {}
 ) {
     val lazyListState = rememberLazyListState()
@@ -70,6 +71,7 @@ fun PairingScreen(
         onClick = onClick,
         onWifiSettingsClick = onWifiSettingsClick,
         onNotificationSettingsClick = onNotificationSettingsClick,
+        onLocalNetworkPermissionClick = onLocalNetworkPermissionClick,
         onRefresh = onRefresh
     )
 }
@@ -82,6 +84,7 @@ private fun PairingScreenContent(
     onClick: (String) -> Unit,
     onWifiSettingsClick: () -> Unit,
     onNotificationSettingsClick: () -> Unit,
+    onLocalNetworkPermissionClick: () -> Unit,
     onRefresh: () -> Unit = {}
 ) {
     PullToRefreshBox(
@@ -102,6 +105,7 @@ private fun PairingScreenContent(
                     uiState = uiState,
                     onWifiSettingsClick = onWifiSettingsClick,
                     onNotificationSettingsClick = onNotificationSettingsClick,
+                    onLocalNetworkPermissionClick = onLocalNetworkPermissionClick
                 )
             }
 
@@ -171,8 +175,29 @@ private fun PairingExplanations(
     uiState: PairingUiState,
     onWifiSettingsClick: () -> Unit,
     onNotificationSettingsClick: () -> Unit,
+    onLocalNetworkPermissionClick: () -> Unit
 ) {
     Column {
+        if (!uiState.hasLocalNetworkPermission) {
+            PairingExplanationRow(
+                text = stringResource(id = R.string.pairing_missing_local_network_permission),
+                icon = R.drawable.ic_warning,
+                onClick = onLocalNetworkPermissionClick
+            )
+        }
+
+        if (!uiState.hasNotificationsPermission) {
+            PairingExplanationRow(
+                text = stringResource(R.string.no_notifications),
+                icon = R.drawable.ic_warning,
+                onClick = onNotificationSettingsClick
+            )
+        }
+
+        if (!uiState.hasLocalNetworkPermission || !uiState.hasNotificationsPermission) {
+            return
+        }
+
         if (uiState.hasDuplicateNames) {
             DuplicateNamesWarning()
         }
@@ -180,13 +205,7 @@ private fun PairingExplanations(
         val someDevicesReachable = uiState.available.isNotEmpty() || uiState.connected.isNotEmpty()
 
         if (someDevicesReachable || uiState.isWifiAvailable) {
-            if (!uiState.hasNotificationsPermission) {
-                PairingExplanationRow(
-                    text = stringResource(R.string.no_notifications),
-                    icon = R.drawable.ic_warning,
-                    onClick = onNotificationSettingsClick
-                )
-            } else if (uiState.isTrustedNetwork) {
+            if (uiState.isTrustedNetwork) {
                 PairingExplanationRow(text = stringResource(R.string.pairing_description))
             } else {
                 PairingExplanationRow(
@@ -315,6 +334,7 @@ private fun PreviewCompose() {
             uiState = PairingUiState(
                 isWifiAvailable = true,
                 hasNotificationsPermission = true,
+                hasLocalNetworkPermission = true,
                 isTrustedNetwork = true,
                 hasDuplicateNames = true,
                 connected = emptyList(),
@@ -341,6 +361,7 @@ private fun PreviewCompose() {
             onClick = { /* Do nothing */ },
             onWifiSettingsClick = { /* Do nothing */ },
             onNotificationSettingsClick = { /* Do nothing */ },
+            onLocalNetworkPermissionClick = { /* Do nothing */ },
             onRefresh = { /* Do nothing */ }
         )
     }
diff --git a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingUiState.kt b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingUiState.kt
index 963121172..9877d96ed 100644
--- a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingUiState.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingUiState.kt
@@ -11,6 +11,7 @@ import org.kde.kdeconnect.ui.compose.model.device.DeviceUiModel
 data class PairingUiState(
     val isWifiAvailable: Boolean,
     val hasNotificationsPermission: Boolean,
+    val hasLocalNetworkPermission: Boolean,
     val isTrustedNetwork: Boolean,
     val hasDuplicateNames: Boolean,
     val connected: List<DeviceUiModel>,
diff --git a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingViewModel.kt b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingViewModel.kt
index 2b897ad91..5a7064063 100644
--- a/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingViewModel.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/compose/screen/pairing/PairingViewModel.kt
@@ -27,6 +27,7 @@ class PairingViewModel(application: Application) : AndroidViewModel(application)
         value = PairingUiState(
             isWifiAvailable = false,
             hasNotificationsPermission = false,
+            hasLocalNetworkPermission = false,
             isTrustedNetwork = false,
             hasDuplicateNames = false,
             connected = emptyList(),
@@ -55,10 +56,12 @@ class PairingViewModel(application: Application) : AndroidViewModel(application)
 
     fun updatePermissions(
         hasNotificationsPermission: Boolean,
+        hasLocalNetworkPermission: Boolean,
     ) {
         _pairingUiState.update {
             it.copy(
                 hasNotificationsPermission = hasNotificationsPermission,
+                hasLocalNetworkPermission = hasLocalNetworkPermission,
             )
         }
     }
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index dc38b0e18..d506338fe 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -248,6 +248,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
     <string name="show_keyboard">Show Keyboard</string>
     <string name="show_mouse_and_keyboard">Show Mouse and Keyboard</string>
     <string name="device_not_paired">Device not paired</string>
+    <string name="pairing_missing_local_network_permission">Missing permission to access nearby devices, the app will not work.</string>
     <string name="pairing_duplicate_names">Caution: There are multiple devices with the same name.</string>
     <string name="request_pairing">Request pairing</string>
     <string name="pairing_accept">Accept</string>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.