[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>