[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