[network/kdeconnect-android] src/main: Migrate pairing screens to Compose

Albert Vaca Cintora <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit a1b9f9428238c40ed8c92404c700562f9e8e513d by Albert Vaca Cintora, on behalf of Saul Cintero.
Committed on 06/08/2026 at 14:33.
Pushed by albertvaka into branch 'master'.

Migrate pairing screens to Compose

Integrate the previously introduced Compose device screens into `DeviceFragment`:

* `DevicePairingScreen`
* `DeviceErrorScreen`

M  +81   -106  src/main/java/org/kde/kdeconnect/ui/DeviceFragment.kt
M  +5    -2    src/main/java/org/kde/kdeconnect/ui/compose/screen/device/DeviceErrorScreen.kt
M  +3    -28   src/main/res/layout/activity_device.xml

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

diff --git a/src/main/java/org/kde/kdeconnect/ui/DeviceFragment.kt b/src/main/java/org/kde/kdeconnect/ui/DeviceFragment.kt
index e93bae70e..7da43afa4 100644
--- a/src/main/java/org/kde/kdeconnect/ui/DeviceFragment.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/DeviceFragment.kt
@@ -7,6 +7,8 @@ package org.kde.kdeconnect.ui
 
 import android.content.Intent
 import android.os.Bundle
+import android.content.Context
+import android.os.Build
 import android.util.Log
 import android.view.LayoutInflater
 import android.view.Menu
@@ -14,6 +16,8 @@ import android.view.MenuInflater
 import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
 import androidx.activity.OnBackPressedCallback
 import androidx.annotation.StringRes
 import androidx.annotation.UiThread
@@ -31,6 +35,12 @@ import org.kde.kdeconnect.helpers.security.SslHelper
 import org.kde.kdeconnect.plugins.Plugin
 import org.kde.kdeconnect.plugins.battery.BatteryPlugin
 import org.kde.kdeconnect.ui.compose.KdeTheme
+import androidx.compose.runtime.getValue
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.kde.kdeconnect.ui.compose.screen.device.DeviceErrorScreen
+import org.kde.kdeconnect.ui.compose.screen.device.DevicePairingScreen
+import org.kde.kdeconnect.ui.compose.screen.device.DeviceViewModel
 import org.kde.kdeconnect.ui.compose.screen.device.PluginsScreen
 import org.kde.kdeconnect_tp.R
 import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding
@@ -63,19 +73,7 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
 
     private val device by lazy { KdeConnect.getInstance().getDevice(deviceId) }
 
-    /**
-     * Not-yet-paired ViewBinding.
-     *
-     * Used to start and retry pairing.
-     */
-    private val pairingBinding get() = binding.pairRequest
-
-    /**
-     * Cannot-communicate ViewBinding.
-     *
-     * Used when the remote device is unreachable.
-     */
-    private val errorBinding get() = binding.pairError
+    private val viewModel by viewModels<DeviceViewModel>()
 
     override fun onInflateBinding(
         inflater: LayoutInflater,
@@ -169,28 +167,6 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         binding.deviceView.setupBottomPadding()
-        errorBinding.errorMessageContainer.setOnRefreshListener {
-            this.refreshDevicesAction()
-        }
-        pairingBinding.pairButton.setOnClickListener {
-            device?.requestPairing()
-            refreshUI()
-        }
-        pairingBinding.acceptButton.setOnClickListener {
-            device?.apply {
-                acceptPairing()
-                pairingBinding.pairingButtons.visibility = View.GONE
-            }
-        }
-        pairingBinding.rejectButton.setOnClickListener {
-            device?.apply {
-                // Remove listener so buttons don't show for an instant before changing the view
-                removePluginsChangedListener(pluginsChangedListener)
-                removePairingCallback(pairingCallback)
-                cancelPairing()
-            }
-            (mActivity as? MainActivity)?.onDeviceSelected(null)
-        }
         device?.apply {
             mActivity?.supportActionBar?.title = name
             removePairingCallback(pairingCallback)
@@ -207,16 +183,16 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
 
     private fun refreshDevicesAction() {
         BackgroundService.ForceRefreshConnections(requireContext())
-        errorBinding.errorMessageContainer.isRefreshing = true
-        errorBinding.errorMessageContainer.postDelayed({
+        viewModel.setRefreshing(refreshing = true)
+        binding.deviceView.postDelayed({
             if (isResumed && !isDetached) { // the view might be destroyed by now
-                errorBinding.errorMessageContainer.isRefreshing = false
+                viewModel.setRefreshing(refreshing = false)
             }
         }, 1500)
     }
 
     override fun onPause() {
-        errorBinding.errorMessageContainer.isRefreshing = false
+        viewModel.setRefreshing(refreshing = false)
         super.onPause()
     }
 
@@ -247,56 +223,26 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
         //Once in-app, there is no point in keep displaying the notification if any
         device.hidePairingNotification()
 
-        when (device.pairStatus) {
-            PairingHandler.PairState.NotPaired -> {
-                errorBinding.errorMessageContainer.visibility = View.GONE
-                binding.deviceView.visibility = View.GONE
-                pairingBinding.pairingButtons.visibility = View.VISIBLE
-                pairingBinding.pairVerification.visibility = View.GONE
-            }
-
-            PairingHandler.PairState.Requested -> {
-                with(pairingBinding) {
-                    pairButton.visibility = View.GONE
-                    pairMessage.text = getString(R.string.pair_requested)
-                    pairProgress.visibility = View.VISIBLE
-                    pairVerification.text = device.verificationKey
-                    pairVerification.visibility = View.VISIBLE
-                }
-            }
-
-            PairingHandler.PairState.RequestedByPeer -> {
-                with(pairingBinding) {
-                    pairMessage.setText(R.string.pair_requested)
-                    pairVerification.visibility = View.VISIBLE
-                    pairingButtons.visibility = View.VISIBLE
-                    pairProgress.visibility = View.GONE
-                    pairButton.visibility = View.GONE
-                    pairRequestButtons.visibility = View.VISIBLE
-                    pairVerification.text = device.verificationKey
-                    pairVerification.visibility = View.VISIBLE
-                }
-                binding.deviceView.visibility = View.GONE
-            }
-
-            PairingHandler.PairState.Paired -> {
-                pairingBinding.pairingButtons.visibility = View.GONE
-                if (device.isReachable) {
-                    val context = requireContext()
-                    val pluginsWithButtons =
-                        device.loadedPlugins.values.flatMap { it.getUiButtons() }
-                    val pluginsNeedPermissions =
-                        device.pluginsWithoutPermissions.values.filter { device.isPluginEnabled(it.pluginKey) }
-                    val pluginsNeedOptionalPermissions =
-                        device.pluginsWithoutOptionalPermissions.values.filter {
-                            device.isPluginEnabled(it.pluginKey)
-                        }
-                    errorBinding.errorMessageContainer.visibility = View.GONE
-                    binding.deviceView.visibility = View.VISIBLE
-                    binding.deviceViewCompose.apply {
-                        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
-                        setContent {
-                            KdeTheme(context) {
+        binding.deviceView.apply {
+            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+            setContent {
+                KdeTheme(context) {
+                    val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
+                    val pairingErrorMessage by viewModel.pairingErrorMessage.collectAsStateWithLifecycle()
+
+                    when (device.pairStatus) {
+                        PairingHandler.PairState.Paired -> {
+                            if (device.isReachable) {
+                                val pluginsWithButtons =
+                                    device.loadedPlugins.values.flatMap { plugin -> plugin.getUiButtons() }
+                                val pluginsNeedPermissions =
+                                    device.pluginsWithoutPermissions.values.filter { plugin ->
+                                        device.isPluginEnabled(plugin.pluginKey)
+                                    }
+                                val pluginsNeedOptionalPermissions =
+                                    device.pluginsWithoutOptionalPermissions.values.filter { plugin ->
+                                        device.isPluginEnabled(plugin.pluginKey)
+                                    }
                                 PluginsScreen(
                                     pluginsWithButtons = pluginsWithButtons,
                                     pluginsNeedPermissions = pluginsNeedPermissions,
@@ -315,13 +261,41 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
                                         )
                                     }
                                 )
+                                displayBatteryInfoIfPossible()
+                            } else {
+                                DeviceErrorScreen(
+                                    isRefreshing = isRefreshing,
+                                    onRefresh = { refreshDevicesAction() }
+                                )
                             }
                         }
+
+                        else -> {
+                            DevicePairingScreen(
+                                pairStatus = device.pairStatus,
+                                verificationKey = device.verificationKey ?: "",
+                                pairMessage = pairingErrorMessage,
+                                onRequestPairing = {
+                                    viewModel.setPairingErrorMessage(message =null)
+                                    device.requestPairing()
+                                    refreshUI()
+                                },
+                                onAcceptPairing = {
+                                    device.acceptPairing()
+                                    refreshUI()
+                                },
+                                onRejectPairing = {
+                                    device.apply {
+                                        // Remove listener so buttons don't show for an instant before changing the view
+                                        removePluginsChangedListener(pluginsChangedListener)
+                                        removePairingCallback(pairingCallback)
+                                        cancelPairing()
+                                    }
+                                    (mActivity as? MainActivity)?.onDeviceSelected(null)
+                                }
+                            )
+                        }
                     }
-                    displayBatteryInfoIfPossible()
-                } else {
-                    errorBinding.errorMessageContainer.visibility = View.VISIBLE
-                    binding.deviceView.visibility = View.GONE
                 }
             }
         }
@@ -335,30 +309,31 @@ class DeviceFragment : BaseFragment<ActivityDeviceBinding>() {
             }
 
             override fun pairingSuccessful() {
-                pairingBinding.pairMessage.announceForAccessibility(getString(R.string.pair_succeeded))
+                val accessibilityManager =
+                    requireContext().getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
+                if (accessibilityManager?.isEnabled == true) {
+                    @Suppress("DEPRECATION")
+                    val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                        AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+                    } else {
+                        AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+                    }
+                    event.text.add(getString(R.string.pair_succeeded))
+                    accessibilityManager.sendAccessibilityEvent(event)
+                }
                 mActivity?.runOnUiThread { refreshUI() }
             }
 
             override fun pairingFailed(error: String) {
                 mActivity?.runOnUiThread {
-                    with(pairingBinding) {
-                        pairMessage.text = error
-                        pairProgress.visibility = View.GONE
-                        pairButton.visibility = View.VISIBLE
-                        pairRequestButtons.visibility = View.GONE
-                    }
+                    viewModel.setPairingErrorMessage(message =error)
                     refreshUI()
                 }
             }
 
             override fun unpaired(device: Device) {
                 mActivity?.runOnUiThread {
-                    with(pairingBinding) {
-                        pairMessage.setText(R.string.device_not_paired)
-                        pairProgress.visibility = View.GONE
-                        pairButton.visibility = View.VISIBLE
-                        pairRequestButtons.visibility = View.GONE
-                    }
+                    viewModel.setPairingErrorMessage(message =null)
                     refreshUI()
                 }
             }
diff --git a/src/main/java/org/kde/kdeconnect/ui/compose/screen/device/DeviceErrorScreen.kt b/src/main/java/org/kde/kdeconnect/ui/compose/screen/device/DeviceErrorScreen.kt
index f23bcbfc7..dfa9f48f1 100644
--- a/src/main/java/org/kde/kdeconnect/ui/compose/screen/device/DeviceErrorScreen.kt
+++ b/src/main/java/org/kde/kdeconnect/ui/compose/screen/device/DeviceErrorScreen.kt
@@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.runtime.Composable
@@ -22,7 +24,7 @@ import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import org.kde.kdeconnect.ui.compose.KdeTheme
-import org.kde.kdeconnect.ui.compose.components.KdeBodyMediumText
+import org.kde.kdeconnect.ui.compose.components.KdeBodyLargeText
 import org.kde.kdeconnect.ui.compose.components.KdeThemePreviews
 import org.kde.kdeconnect_tp.R
 
@@ -50,6 +52,7 @@ private fun DeviceErrorScreenContent(
         Row(
             modifier = Modifier
                 .fillMaxSize()
+                .verticalScroll(state = rememberScrollState())
                 .padding(all = 16.dp),
             horizontalArrangement = Arrangement.Center,
             verticalAlignment = Alignment.CenterVertically
@@ -60,7 +63,7 @@ private fun DeviceErrorScreenContent(
                 colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurfaceVariant),
                 modifier = Modifier.padding(end = 8.dp)
             )
-            KdeBodyMediumText(
+            KdeBodyLargeText(
                 text = stringResource(id = R.string.unreachable_description),
                 modifier = Modifier.weight(1f)
             )
diff --git a/src/main/res/layout/activity_device.xml b/src/main/res/layout/activity_device.xml
index 4596557be..b84a20dfb 100644
--- a/src/main/res/layout/activity_device.xml
+++ b/src/main/res/layout/activity_device.xml
@@ -9,37 +9,12 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:descendantFocusability="afterDescendants"
-    android:focusable="false"
     android:layout_height="match_parent"
     tools:context="org.kde.kdeconnect.ui.DeviceFragment">
 
-    <!-- Shown when device is reachable but not yet paired -->
-    <include
-        android:id="@+id/pair_request"
-        layout="@layout/view_pair_request"
-        tools:visibility="gone"/>
-
-    <!-- Shown when the device is paired but not reachable -->
-    <include
-        android:id="@+id/pair_error"
-        layout="@layout/view_pair_error"
-        tools:visibility="gone"/>
-
-    <androidx.core.widget.NestedScrollView
+    <androidx.compose.ui.platform.ComposeView
         android:id="@+id/device_view"
-        android:descendantFocusability="afterDescendants"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipToPadding="false">
-
-         <!-- Shown when the device is paired and reachable -->
-        <androidx.compose.ui.platform.ComposeView
-            android:id="@+id/device_view_compose"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
-
-    </androidx.core.widget.NestedScrollView>
-</FrameLayout>
-
+        android:layout_height="match_parent" />
 
+</FrameLayout>
\ No newline at end of file
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.