Repository: call-gate-app/android-app
Pull request #6: Add connection module to display and copy local and public IP addresses
Author: login: capcom6
Created: 2025-04-18T10:52:50Z | Merged: 2025-04-21T12:44:07Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added display of local and public IP addresses in the Home screen, with the ability to copy addresses to the clipboard.
  - Introduced a new card-style UI section showing server information, including IP addresses, username, and password.
- **Improvements**
  - Enhanced Home screen layout for better organization and visibility of server details.
  - Added live updating of server credentials and IP addresses on Home screen resume.
  - Integrated Android Navigation Component for smoother navigation and UI synchronization.
  - Added navigation to server and webhook settings from the Settings screen.
- **Permissions**
  - App now requests access to Wi-Fi state for improved network information.
- **User Interface**
  - Added new icons and string resources for clearer feedback and interaction.
- **Bug Fixes**
  - Fixed settings navigation to use navigation controller for better consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 753aadd..ab423e8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -63,6 +63,7 @@ dependencies {
     val koinVersion = "4.0.4"
     val roomVersion = "2.6.1"
     val workVersion = "2.9.1"
+    val navigationVersion = "2.8.9"
 
     implementation("androidx.core:core-ktx:1.13.1")
     implementation("androidx.appcompat:appcompat:1.7.0")
@@ -70,6 +71,9 @@ dependencies {
     implementation("androidx.constraintlayout:constraintlayout:2.2.1")
     implementation("androidx.preference:preference-ktx:1.2.1")
 
+    implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
+    implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")
+
     // Coroutines
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c7970b3..370f1d0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
         android:name="android.hardware.telephony"
         android:required="true" />
 
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
diff --git a/app/src/main/java/app/callgate/android/App.kt b/app/src/main/java/app/callgate/android/App.kt
index cc9d3b0..f14776f 100644
--- a/app/src/main/java/app/callgate/android/App.kt
+++ b/app/src/main/java/app/callgate/android/App.kt
@@ -2,6 +2,7 @@ package app.callgate.android
 
 import android.app.Application
 import app.callgate.android.modules.calls.callsModule
+import app.callgate.android.modules.connection.connectionModule
 import app.callgate.android.modules.db.dbModule
 import app.callgate.android.modules.notifications.notificationsModule
 import app.callgate.android.modules.orchestrator.OrchestratorService
@@ -28,6 +29,7 @@ class App : Application() {
                 serverService,
                 callsModule,
                 webhooksModule,
+                connectionModule,
                 orchestratorModule,
             )
         }
diff --git a/app/src/main/java/app/callgate/android/MainActivity.kt b/app/src/main/java/app/callgate/android/MainActivity.kt
index cd6bd37..b226bc9 100644
--- a/app/src/main/java/app/callgate/android/MainActivity.kt
+++ b/app/src/main/java/app/callgate/android/MainActivity.kt
@@ -1,9 +1,10 @@
 package app.callgate.android
 
-import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.setupWithNavController
 import app.callgate.android.databinding.ActivityMainBinding
-import app.callgate.android.ui.HomeFragment
 
 class MainActivity : AppCompatActivity() {
     private lateinit var binding: ActivityMainBinding
@@ -14,9 +15,10 @@ class MainActivity : AppCompatActivity() {
         binding = ActivityMainBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
-        supportFragmentManager
-            .beginTransaction()
-            .replace(R.id.container, HomeFragment())
-            .commit()
+        val navHostFragment = supportFragmentManager
+            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+        val navController = navHostFragment.navController
+
+        binding.topAppBar.setupWithNavController(navController)
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt b/app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt
new file mode 100644
index 0000000..9e0f50a
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt
@@ -0,0 +1,15 @@
+package app.callgate.android.modules.connection
+
+import android.content.Context
+import app.callgate.android.modules.connection.ip.LocalIPProvider
+import app.callgate.android.modules.connection.ip.PublicIPProvider
+
+class ConnectionService(
+    context: Context
+) {
+    private val localIPProvider = LocalIPProvider(context)
+    private val publicIPProvider = PublicIPProvider()
+
+    suspend fun getLocalIP(): String? = localIPProvider.getIP()
+    suspend fun getPublicIP(): String? = publicIPProvider.getIP()
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt b/app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt
new file mode 100644
index 0000000..f3b21ea
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt
@@ -0,0 +1,30 @@
+package app.callgate.android.modules.connection
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ConnectionViewModel(
+    private val connectionSvc: ConnectionService
+) : ViewModel() {
+    private val _localIP = MutableLiveData<String>()
+    val localIP: LiveData<String> = _localIP
+
+    private val _publicIP = MutableLiveData<String>()
+    val publicIP: LiveData<String> = _publicIP
+
+    fun getLocalIP() {
+        viewModelScope.launch(Dispatchers.IO) {
+            _localIP.postValue(connectionSvc.getLocalIP())
+        }
+    }
+
+    fun getPublicIP() {
+        viewModelScope.launch(Dispatchers.IO) {
+            _publicIP.postValue(connectionSvc.getPublicIP())
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/Module.kt b/app/src/main/java/app/callgate/android/modules/connection/Module.kt
new file mode 100644
index 0000000..6050c58
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/Module.kt
@@ -0,0 +1,12 @@
+package app.callgate.android.modules.connection
+
+import org.koin.core.module.dsl.singleOf
+import org.koin.core.module.dsl.viewModelOf
+import org.koin.dsl.module
+
+val connectionModule = module {
+    singleOf(::ConnectionService)
+    viewModelOf(::ConnectionViewModel)
+}
+
+val MODULE_NAME = "connection"
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt b/app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt
new file mode 100644
index 0000000..758d856
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt
@@ -0,0 +1,5 @@
+package app.callgate.android.modules.connection.ip
+
+interface IPProvider {
+    suspend fun getIP(): String?
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt b/app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt
new file mode 100644
index 0000000..f1bd174
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt
@@ -0,0 +1,51 @@
+package app.callgate.android.modules.connection.ip
+
+import android.content.Context
+import android.net.wifi.WifiManager
+import java.net.Inet4Address
+import java.net.NetworkInterface
+
+
+class LocalIPProvider(private val context: Context) : IPProvider {
+    override suspend fun getIP(): String? {
+        try {
+            val wifiManager =
+                context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
+
+            // If the device is in tethering mode, the following method may return null or a default IP
+            wifiManager.connectionInfo?.let { connectionInfo ->
+                val ipInt = connectionInfo.ipAddress
+                if (ipInt != 0) {
+                    return java.net.InetAddress.getByAddress(
+                        byteArrayOf(
+                            (ipInt and 0xFF).toByte(),
+                            ((ipInt shr 8) and 0xFF).toByte(),
+                            ((ipInt shr 16) and 0xFF).toByte(),
+                            ((ipInt shr 24) and 0xFF).toByte()
+                        )
+                    ).hostAddress
+                }
+            }
+
+            // If the above doesn't work, try to find the WiFi network interface directly
+            val wifiInterface = NetworkInterface.getNetworkInterfaces().asSequence()
+                .find { it.name.contains("wlan", ignoreCase = true) }
+
+            wifiInterface?.inetAddresses?.asSequence()
+                ?.find { !it.isLoopbackAddress && it is Inet4Address }
+                ?.let { inetAddress ->
+                    return inetAddress.hostAddress
+                }
+
+            // Check any other network interface
+            NetworkInterface.getNetworkInterfaces().asSequence()
+                .flatMap { intf -> intf.inetAddresses.asSequence() }
+                .find { !it.isLoopbackAddress && it is Inet4Address }
+                ?.let { return it.hostAddress }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+
+        return null
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt b/app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
new file mode 100644
index 0000000..46af2ad
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
@@ -0,0 +1,31 @@
+package app.callgate.android.modules.connection.ip
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.request.get
+
+class PublicIPProvider : IPProvider {
+    override suspend fun getIP(): String? {
+        val client = HttpClient(OkHttp) {
+            install(HttpTimeout) {
+                requestTimeoutMillis = REQUEST_TIMEOUT_MS
+            }
+        }
+
+        return try {
+            client.get(PUBLIC_IP_URL).body<String>()
+        } catch (e: Exception) {
+            e.printStackTrace()
+            null
+        } finally {
+            client.close()
+        }
+    }
+
+    companion object {
+        private const val PUBLIC_IP_URL = "https://api.ipify.org"
+        private const val REQUEST_TIMEOUT_MS = 5000L
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/server/Module.kt b/app/src/main/java/app/callgate/android/modules/server/Module.kt
index 04d19f7..a4f0fca 100644
--- a/app/src/main/java/app/callgate/android/modules/server/Module.kt
+++ b/app/src/main/java/app/callgate/android/modules/server/Module.kt
@@ -1,8 +1,10 @@
 package app.callgate.android.modules.server
 
 import org.koin.core.module.dsl.singleOf
+import org.koin.core.module.dsl.viewModelOf
 import org.koin.dsl.module
 
 val serverService = module {
     singleOf(::ServerService)
+    viewModelOf(::ServerViewModel)
 }
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/modules/server/ServerService.kt b/app/src/main/java/app/callgate/android/modules/server/ServerService.kt
index 47217c5..9fcbf53 100644
--- a/app/src/main/java/app/callgate/android/modules/server/ServerService.kt
+++ b/app/src/main/java/app/callgate/android/modules/server/ServerService.kt
@@ -4,7 +4,7 @@ import android.content.Context
 import android.os.Build
 
 class ServerService(
-    private val settings: ServerSettings,
+    val settings: ServerSettings,
 ) {
     fun start(context: Context) {
         settings.deviceId = settings.deviceId ?: getDeviceId(context)
diff --git a/app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt b/app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt
new file mode 100644
index 0000000..c129527
--- /dev/null
+++ b/app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt
@@ -0,0 +1,30 @@
+package app.callgate.android.modules.server
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class ServerViewModel(
+    private val serverSvc: ServerService
+) : ViewModel() {
+    private val _credentials = MutableLiveData<Credentials>()
+    val credentials: LiveData<Credentials> = _credentials
+
+    fun refresh() {
+        _credentials.postValue(
+            Credentials(
+                serverSvc.settings.username,
+                serverSvc.settings.password,
+            )
+        )
+    }
+
+    init {
+        refresh()
+    }
+
+    data class Credentials(
+        val username: String,
+        val password: String,
+    )
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/callgate/android/ui/HomeFragment.kt b/app/src/main/java/app/callgate/android/ui/HomeFragment.kt
index 1eb662d..a6cd73f 100644
--- a/app/src/main/java/app/callgate/android/ui/HomeFragment.kt
+++ b/app/src/main/java/app/callgate/android/ui/HomeFragment.kt
@@ -1,23 +1,36 @@
 package app.callgate.android.ui
 
 import android.Manifest
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
+import android.text.Html
+import android.text.SpannableStringBuilder
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.URLSpan
 import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.TextView
 import android.widget.Toast
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.core.content.ContextCompat
+import androidx.core.text.toSpanned
 import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
 import app.callgate.android.R
 import app.callgate.android.databinding.FragmentHomeBinding
+import app.callgate.android.modules.connection.ConnectionViewModel
 import app.callgate.android.modules.orchestrator.OrchestratorService
 import app.callgate.android.modules.server.ServerService
-import app.callgate.android.ui.settings.SettingsFragment
+import app.callgate.android.modules.server.ServerViewModel
 import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
 class HomeFragment : Fragment() {
     private var _binding: FragmentHomeBinding? = null
@@ -26,6 +39,9 @@ class HomeFragment : Fragment() {
     private val orchestratorSvc: OrchestratorService by inject()
     private val serverSvc: ServerService by inject()
 
+    private val connectionVM: ConnectionViewModel by viewModel()
+    private val serverVM: ServerViewModel by viewModel()
+
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
         savedInstanceState: Bundle?
@@ -37,21 +53,46 @@ class HomeFragment : Fragment() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        binding.actionSettings.setOnClickListener {
-            parentFragmentManager
-                .beginTransaction()
-                .replace(R.id.container, SettingsFragment.newInstance())
-                .addToBackStack(null)
-                .commit()
+        connectionVM.localIP.observe(viewLifecycleOwner) {
+            if (it != null) {
+                binding.textLocalIP.setCopyableText("$it:${serverSvc.settings.port}")
+            } else {
+                binding.textLocalIP.text = getString(R.string.n_a)
+            }
         }
 
-        binding.toggleOnline.setOnClickListener {
-            actionStart(binding.toggleOnline.isChecked)
+        connectionVM.publicIP.observe(viewLifecycleOwner) {
+            if (it != null) {
+                binding.textPublicIP.setCopyableText("$it:${serverSvc.settings.port}")
+            } else {
+                binding.textPublicIP.text = getString(R.string.n_a)
+            }
+        }
+
+        serverVM.credentials.observe(viewLifecycleOwner) {
+            binding.textLocalUsername.setCopyableText(it.username)
+            binding.textLocalPassword.setCopyableText(it.password)
         }
 
         serverSvc.isActiveLiveData().observe(viewLifecycleOwner) {
             binding.toggleOnline.isChecked = it
         }
+
+        binding.actionSettings.setOnClickListener {
+            findNavController().navigate(R.id.action_home_to_settings)
+        }
+
+        binding.toggleOnline.setOnClickListener {
+            actionStart(binding.toggleOnline.isChecked)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        connectionVM.getLocalIP()
+        connectionVM.getPublicIP()
+        serverVM.refresh()
     }
 
     override fun onDestroyView() {
@@ -108,6 +149,53 @@ class HomeFragment : Fragment() {
         }
     }
 
+    private fun TextView.setCopyableText(text: String) {
+        val source = Html.fromHtml("<a href>$text</a>")
+        val builder = SpannableStringBuilder(source)
+        val spans = builder.getSpans(0, builder.length, URLSpan::class.java)
+        for (span in spans) {
+            val innerText = builder.subSequence(
+                builder.getSpanStart(span),
+                builder.getSpanEnd(span)
+            ).toString()
+            val clickableSpan = object : ClickableSpan() {
+
+                override fun onClick(widget: View) {
+                    try {
+                        val clipboard = requireContext().getSystemService(
+                            Context.CLIPBOARD_SERVICE
+                        ) as ClipboardManager
+                        clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
+                        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+                            Toast.makeText(
+                                context,
+                                R.string.copied_to_clipboard,
+                                Toast.LENGTH_SHORT
+                            ).show()
+                        }
+                    } catch (e: Exception) {
+                        Log.e("HomeFragment", "Failed to copy text to clipboard", e)
+                        Toast.makeText(
+                            context,
+                            context.getString(R.string.failed_to_copy_to_clipboard),
+                            Toast.LENGTH_SHORT
+                        ).show()
+                    }
+                }
+            }
+            builder.setSpan(
+                clickableSpan,
+                builder.getSpanStart(span),
+                builder.getSpanEnd(span),
+                builder.getSpanFlags(span)
+            )
+            builder.removeSpan(span)
+        }
+
+        this.movementMethod = LinkMovementMethod.getInstance()
+        this.text = builder.toSpanned()
+    }
+
     companion object {
         fun newInstance() =
             HomeFragment()
diff --git a/app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt b/app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt
index cb0c93e..2254c30 100644
--- a/app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt
+++ b/app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt
@@ -7,6 +7,7 @@ import android.widget.EditText
 import android.widget.Toast
 import androidx.preference.PreferenceFragmentCompat
 
+
 abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
diff --git a/app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt b/app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt
index 5ad02f2..583e80e 100644
--- a/app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt
@@ -1,6 +1,7 @@
 package app.callgate.android.ui.settings
 
 import android.os.Bundle
+import androidx.navigation.fragment.findNavController
 import androidx.preference.Preference
 import app.callgate.android.BuildConfig
 import app.callgate.android.R
@@ -10,11 +11,17 @@ class SettingsFragment : BasePreferenceFragment() {
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         setPreferencesFromResource(R.xml.root_preferences, rootKey)
 
+        findPreference<Preference>("navigation.server")?.setOnPreferenceClickListener {
+            findNavController().navigate(R.id.action_settings_to_server_settings)
+            true
+        }
+
+        findPreference<Preference>("navigation.webhooks")?.setOnPreferenceClickListener {
+            findNavController().navigate(R.id.action_settings_to_webhooks_settings)
+            true
+        }
+
         findPreference<Preference>("transient.app_version")?.summary =
             "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
     }
-
-    companion object {
-        fun newInstance() = SettingsFragment()
-    }
 }
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index d77cff4..1367dd3 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,13 +1,27 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:orientation="vertical"
     tools:context=".MainActivity">
 
-    <FrameLayout
-        android:id="@+id/container"
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/topAppBar"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="?attr/actionBarSize"
+        android:background="?attr/colorPrimary"
+        app:menu="@menu/nav_menu"
+        app:title="@string/app_name"
+        app:titleCentered="true" />
 
-</FrameLayout>
\ No newline at end of file
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/nav_host_fragment"
+        android:name="androidx.navigation.fragment.NavHostFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:defaultNavHost="true"
+        app:navGraph="@navigation/nav_graph" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index c00cbb3..e29da2f 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -4,10 +4,123 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:gravity="bottom"
     android:orientation="vertical"
     tools:context=".ui.HomeFragment">
 
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        app:cardCornerRadius="16dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="4dp">
+
+            <ImageView
+                android:id="@+id/imageServer"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_margin="4dp"
+                android:src="@drawable/ic_server"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <GridLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:columnCount="2"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@id/imageServer"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_columnSpan="2"
+                    android:text="@string/server"
+                    android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/local_address" />
+
+                <TextView
+                    android:id="@+id/textLocalIP"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/dots"
+                    tools:text="127.0.0.1:8080" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/public_address" />
+
+                <TextView
+                    android:id="@+id/textPublicIP"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/dots"
+                    tools:text="127.0.0.1:8080" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/label_username" />
+
+                <TextView
+                    android:id="@+id/textLocalUsername"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:autoLink="web"
+                    tools:text="call" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/label_password" />
+
+                <TextView
+                    android:id="@+id/textLocalPassword"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:autoLink="web"
+                    tools:text="1q2w3e4r" />
+            </GridLayout>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.cardview.widget.CardView>
+
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
     <Button
         android:id="@+id/actionSettings"
         android:layout_width="match_parent"
diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml
new file mode 100644
index 0000000..e73b6af
--- /dev/null
+++ b/app/src/main/res/menu/nav_menu.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu>
+
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..de7393e
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/nav_graph"
+    app:startDestination="@id/homeFragment">
+
+    <fragment
+        android:id="@+id/homeFragment"
+        android:name="app.callgate.android.ui.HomeFragment"
+        android:label="Home"
+        tools:layout="@layout/fragment_home">
+
+        <action
+            android:id="@+id/action_home_to_settings"
+            app:destination="@id/settingsFragment"
+            app:enterAnim="@android:anim/fade_in"
+            app:exitAnim="@android:anim/fade_out" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/settingsFragment"
+        android:name="app.callgate.android.ui.settings.SettingsFragment"
+        android:label="Settings">
+
+        <action
+            android:id="@+id/action_settings_to_server_settings"
+            app:destination="@id/serverSettingFragment" />
+
+        <action
+            android:id="@+id/action_settings_to_webhooks_settings"
+            app:destination="@id/webhooksSettingsFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/serverSettingFragment"
+        android:name="app.callgate.android.ui.settings.ServerSettingsFragment"
+        android:label="Server Settings" />
+
+    <fragment
+        android:id="@+id/webhooksSettingsFragment"
+        android:name="app.callgate.android.ui.settings.WebhooksSettingsFragment"
+        android:label="Webhooks Settings" />
+</navigation>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2357f0f..755fab2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,12 +3,17 @@
     <string name="app_version_build">App version (build)</string>
     <string name="autostart">Autostart</string>
     <string name="call_gate_notifications">Call Gate Notifications</string>
+    <string name="copied_to_clipboard">Copied</string>
     <string name="credentials">Credentials</string>
+    <string name="dots">…</string>
+    <string name="failed_to_copy_to_clipboard">Failed to copy to clipboard</string>
     <string name="general">General</string>
     <string name="information">Information</string>
     <string name="is_not_a_valid_port_must_be_between_1024_and_65535">%1$s is not a valid port. Must be between 1024 and 65535</string>
     <string name="label_password">Password:</string>
     <string name="label_username">Username:</string>
+    <string name="local_address">Local address:</string>
+    <string name="n_a">N/A</string>
     <string name="offline">Offline</string>
     <string name="online">Online</string>
     <string name="password">Password</string>
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 38304ac..3df7973 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -1,12 +1,14 @@
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
     <Preference
+        app:key="navigation.server"
         android:icon="@drawable/ic_server"
         app:fragment="app.callgate.android.ui.settings.ServerSettingsFragment"
         app:summary="@string/port_credentials_etc"
         app:title="@string/server_dotdotdot" />
 
     <Preference
+        app:key="navigation.webhooks"
         android:icon="@drawable/ic_webhook"
         app:fragment="app.callgate.android.ui.settings.WebhooksSettingsFragment"
         app:summary="@string/retries_signing_etc"


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 10**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>app/src/main/java/app/callgate/android/modules/server/ServerService.kt (1)</summary><blockquote>

`7-7`: **Consider documenting the reason for exposing settings**

Changing the visibility of the settings property from private to public is appropriate for the new connection functionality, but it would be helpful to add a comment explaining why external access is needed.

```diff
-    val settings: ServerSettings,
+    /** 
+     * Server settings accessible by connection module to retrieve server configuration
+     * such as port number for IP address display.
+     */
+    val settings: ServerSettings,
```

</blockquote></details>
<details>
<summary>app/src/main/res/layout/fragment_home.xml (1)</summary><blockquote>

`91-97`: **Remove autoLink from username field if it's sensitive**

Consider whether the username is sensitive information. If it is, remove the `autoLink="web"` attribute to prevent parts of the username from becoming clickable links.

```diff
 <TextView
     android:id="@+id/textLocalUsername"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="start"
     android:layout_margin="4dp"
-    android:autoLink="web"
     tools:text="call" />
```

</blockquote></details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt (1)</summary><blockquote>

`17-19`: **Consider alternative IP services for reliability**

The current implementation relies on a single service (api.ipify.org) which could be a single point of failure. Consider implementing a fallback mechanism with alternative IP services.


```diff
    return try {
-        client.get("https://api.ipify.org").body<String>()
+        try {
+            client.get(PRIMARY_IP_URL).body<String>()
+        } catch (e: Exception) {
+            Log.w(TAG, "Primary IP service failed, trying backup", e)
+            client.get(BACKUP_IP_URL).body<String>()
+        }
    } catch (e: Exception) {
```

</blockquote></details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt (2)</summary><blockquote>

`10-24`: **Consider using a more efficient method to convert IP int to string**

While your logic for converting the integer IP address to a string representation is correct, consider using Java's built-in `InetAddress` for a more readable and maintainable solution.


```diff
override suspend fun getIP(): String? {
    try {
        val wifiManager =
            context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager

        // If the device is in tethering mode, the following method may return null or a default IP
        wifiManager.connectionInfo?.let { connectionInfo ->
            val ipInt = connectionInfo.ipAddress
            if (ipInt != 0) {
-                return (ipInt and 0xFF).toString() + "." +
-                        ((ipInt shr 8) and 0xFF) + "." +
-                        ((ipInt shr 16) and 0xFF) + "." +
-                        ((ipInt shr 24) and 0xFF)
+                return java.net.InetAddress.getByAddress(byteArrayOf(
+                    (ipInt and 0xFF).toByte(),
+                    ((ipInt shr 8) and 0xFF).toByte(),
+                    ((ipInt shr 16) and 0xFF).toByte(),
+                    ((ipInt shr 24) and 0xFF).toByte()
+                )).hostAddress
            }
        }
```

---

`37-47`: **Use Kotlin idiomatic approach for iterating through network interfaces**

The while loops for iterating through network interfaces and addresses could be replaced with Kotlin's more idiomatic sequence operations, similar to what you've done in the previous section.


```diff
        // Check any other network interface
-        val interfaces = NetworkInterface.getNetworkInterfaces()
-        while (interfaces.hasMoreElements()) {
-            val intf = interfaces.nextElement()
-            val addrs = intf.inetAddresses
-            while (addrs.hasMoreElements()) {
-                val addr = addrs.nextElement()
-                if (!addr.isLoopbackAddress && addr is Inet4Address) {
-                    return addr.hostAddress
-                }
-            }
-        }
+        NetworkInterface.getNetworkInterfaces().asSequence()
+            .flatMap { intf -> intf.inetAddresses.asSequence() }
+            .find { !it.isLoopbackAddress && it is Inet4Address }
+            ?.let { return it.hostAddress }
```

</blockquote></details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt (1)</summary><blockquote>

`13-14`: **Initialize LiveData with loading state**

Consider initializing the LiveData objects with a loading state or null to make the initial state more explicit.


```diff
-private val _localIP = MutableLiveData<String>()
+private val _localIP = MutableLiveData<String?>(null)
val localIP: LiveData<String> = _localIP

-private val _publicIP = MutableLiveData<String>()
+private val _publicIP = MutableLiveData<String?>(null)
val publicIP: LiveData<String> = _publicIP
```


Also applies to: 16-17

</blockquote></details>
<details>
<summary>app/src/main/java/app/callgate/android/ui/HomeFragment.kt (2)</summary><blockquote>

`54-69`: **Consider adding loading indicators for IP retrieval**

The current implementation shows "N/A" when an IP is null, but there's no distinction between loading state and error state. Consider adding loading indicators or error states to improve user experience.


```diff
connectionVM.localIP.observe(viewLifecycleOwner) {
    if (it != null) {
        binding.textLocalIP.setCopyableText("$it:${serverSvc.settings.port}")
    } else {
-        binding.textLocalIP.text = "N/A"
+        binding.textLocalIP.text = getString(R.string.not_available)
    }
}

connectionVM.publicIP.observe(viewLifecycleOwner) {
    if (it != null) {
        binding.textPublicIP.setCopyableText("$it:${serverSvc.settings.port}")
    } else {
-        binding.textPublicIP.text = "N/A"
+        binding.textPublicIP.text = getString(R.string.not_available)
    }
}
```

---

`90-91`: **Consider adding loading states when fetching IPs**

When IP addresses are being fetched, consider updating the UI to show a loading state to improve user experience.


```diff
override fun onResume() {
    super.onResume()

+    // Show loading state
+    binding.textLocalIP.text = getString(R.string.loading)
+    binding.textPublicIP.text = getString(R.string.loading)
+    
    connectionVM.getLocalIP()
    connectionVM.getPublicIP()
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 73c0f0547f03993f07bfd5d2576b6929c7ca71a1 and 44e886a4658231dcf343a7aef8369d76a22e04eb.

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `app/src/main/AndroidManifest.xml` (1 hunks)
* `app/src/main/java/app/callgate/android/App.kt` (2 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/Module.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/ServerService.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/ui/HomeFragment.kt` (5 hunks)
* `app/src/main/res/layout/fragment_home.xml` (1 hunks)
* `app/src/main/res/values/strings.xml` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>app/src/main/AndroidManifest.xml (1)</summary>

`9-9`: **Good addition of the Wi-Fi state permission**

This permission is necessary for retrieving the device's local IP address through the Wi-Fi manager, which is required for the new connection module functionality.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/App.kt (2)</summary>

`5-5`: **Good import for connection module**

The import correctly brings in the new connection module that will provide IP address functionality.

---

`32-32`: **Good integration of connection module into DI setup**

The connectionModule is properly registered in the Koin dependency injection framework, making its services and view models available throughout the app.

</details>
<details>
<summary>app/src/main/res/values/strings.xml (1)</summary>

`35-37`: **Good addition of new string resources**

These string resources properly support the new UI elements for displaying local and public IP addresses and providing clipboard feedback. The naming is consistent with existing patterns in the file.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt (1)</summary>

`1-5`: **LGTM: Clean interface design with appropriate nullability**

The interface is well-designed with a single responsibility and appropriately uses a suspend function for asynchronous operations. The nullable return type correctly handles cases where IP retrieval might fail.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/Module.kt (2)</summary>

`7-10`: **LGTM: Clean dependency injection setup**

The Koin module is properly configured with appropriate scopes - singleton for the service and viewModel for the ViewModel.

---

`12-12`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider whether this constant is necessary**

The `MODULE_NAME` constant doesn't appear to be used within this file. If it's not referenced elsewhere in the codebase, consider removing it to avoid unused code.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if MODULE_NAME is referenced anywhere in the codebase
rg -i "MODULE_NAME" --type kotlin --files-without-match "app/src/main/java/app/callgate/android/modules/connection/Module.kt"
```

Length of output: 123

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all occurrences of the MODULE_NAME constant across Kotlin and Java files,
# excluding its own declaration in Module.kt
rg -n '\bMODULE_NAME\b' --type kotlin --type java \
  | grep -v 'app/src/main/java/app/callgate/android/modules/connection/Module.kt:12'
```

Length of output: 913

---



</details>

**Remove unused `MODULE_NAME` constant**  

The `MODULE_NAME` property in app/src/main/java/app/callgate/android/modules/connection/Module.kt (line 12) isn’t referenced anywhere else and can be safely removed to clean up unused code.  

• File: app/src/main/java/app/callgate/android/modules/connection/Module.kt  
  ```diff
 - val MODULE_NAME = "connection"
  ```  



> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt (1)</summary>

`1-15`: **LGTM: Clean service implementation with proper delegation**

The service is well-designed with appropriate constructor injection and clean delegation to specialized providers.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt (1)</summary>

`10-10`: <details>
<summary>✅ Verification successful</summary>

**Verify ACCESS_WIFI_STATE permission is in AndroidManifest.xml**

This class uses WifiManager which requires the ACCESS_WIFI_STATE permission.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the permission is properly declared in the manifest
grep -A 1 "ACCESS_WIFI_STATE" app/src/main/AndroidManifest.xml || echo "Permission not found in manifest"
```

Length of output: 215

---



</details>

**ACCESS_WIFI_STATE permission confirmed**  
The `android.permission.ACCESS_WIFI_STATE` entry is present in `app/src/main/AndroidManifest.xml`. No changes required.

</details>
<details>
<summary>app/src/main/java/app/callgate/android/ui/HomeFragment.kt (1)</summary>

`41-41`: <details>
<summary>✅ Verification successful</summary>

**Verify ConnectionViewModel injection is properly setup**

Make sure the ConnectionViewModel is properly registered in the Koin module.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if ConnectionViewModel is registered in the Koin module
grep -r "viewModel" app/src/main/java/app/callgate/android/modules/connection/
```

Length of output: 677

---



</details>

**ConnectionViewModel injection verified**  
The `ConnectionViewModel` is correctly registered in `app/src/main/java/app/callgate/android/modules/connection/Module.kt` using  
```kotlin
viewModelOf(::ConnectionViewModel)
```  
No further changes required.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T10:56:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt (2)</summary><blockquote>

`13-20`: **Consider handling potential null or empty values.**

The current implementation assumes that serverSvc.settings always has valid username and password values. You might want to add null checks or provide default values to prevent potential issues.


```diff
fun refresh() {
    _credentials.postValue(
        Credentials(
-           serverSvc.settings.username,
-           serverSvc.settings.password,
+           serverSvc.settings.username.orEmpty(),
+           serverSvc.settings.password.orEmpty(),
        )
    )
}
```

---

`26-29`: **Consider adding security annotations for credential fields.**

Since this data class contains sensitive information (username and password), consider adding annotations like `@Sensitive` or similar security markers if your project uses any security analysis tools.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 44e886a4658231dcf343a7aef8369d76a22e04eb and bac8df3e24d4567fe90e2ac4307d6445ee1154b0.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/Module.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/ui/HomeFragment.kt` (5 hunks)
* `app/src/main/res/layout/fragment_home.xml` (1 hunks)
* `app/src/main/res/values/strings.xml` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt
* app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
* app/src/main/res/layout/fragment_home.xml
* app/src/main/res/values/strings.xml
* app/src/main/java/app/callgate/android/ui/HomeFragment.kt

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>app/src/main/java/app/callgate/android/modules/server/Module.kt (1)</summary>

`4-4`: **Clean integration of ServerViewModel in the dependency injection system.**

The addition of the ServerViewModel to the Koin module is implemented correctly using the appropriate `viewModelOf` function from the Koin DSL. This ensures the ViewModel will be properly scoped and lifecycle-aware.



Also applies to: 9-9

</details>
<details>
<summary>app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt (1)</summary>

`1-30`: **Good implementation of MVVM pattern with proper LiveData encapsulation.**

The ServerViewModel follows Android architecture best practices:
- Appropriate constructor injection for dependencies
- Proper encapsulation of mutable LiveData with immutable public exposure
- Clean data refresh method with initialization in the init block
- Well-structured data class for the credentials model

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-19T06:52:43Z

Review comments:
diff_hunk: @@ -4,10 +4,123 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:gravity="bottom"
     android:orientation="vertical"
     tools:context=".ui.HomeFragment">
 
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        app:cardCornerRadius="16dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="4dp">
+
+            <ImageView
+                android:id="@+id/imageServer"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_margin="4dp"
+                android:src="@drawable/ic_server"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
path: app/src/main/res/layout/fragment_home.xml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add content description for accessibility**

The ImageView lacks a content description, which is necessary for screen readers and accessibility purposes.

```diff
 <ImageView
     android:id="@+id/imageServer"
     android:layout_width="24dp"
     android:layout_height="24dp"
     android:layout_margin="4dp"
     android:src="@drawable/ic_server"
+    android:contentDescription="@string/server_icon_description"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toTopOf="parent" />
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            <ImageView
                android:id="@+id/imageServer"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_margin="4dp"
                android:src="@drawable/ic_server"
                android:contentDescription="@string/server_icon_description"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -4,10 +4,123 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:gravity="bottom"
     android:orientation="vertical"
     tools:context=".ui.HomeFragment">
 
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        app:cardCornerRadius="16dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="4dp">
+
+            <ImageView
+                android:id="@+id/imageServer"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_margin="4dp"
+                android:src="@drawable/ic_server"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <GridLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:columnCount="2"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@id/imageServer"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_columnSpan="2"
+                    android:text="Server"
+                    android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/local_address" />
+
+                <TextView
+                    android:id="@+id/textLocalIP"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/dots"
+                    tools:text="127.0.0.1:8080" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/public_address" />
+
+                <TextView
+                    android:id="@+id/textPublicIP"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/dots"
+                    tools:text="127.0.0.1:8080" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/label_username" />
+
+                <TextView
+                    android:id="@+id/textLocalUsername"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:autoLink="web"
+                    tools:text="call" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:text="@string/label_password" />
+
+                <TextView
+                    android:id="@+id/textLocalPassword"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:layout_margin="4dp"
+                    android:autoLink="web"
+                    tools:text="1q2w3e4r" />
path: app/src/main/res/layout/fragment_home.xml
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove autoLink from password field**

Using `autoLink="web"` on a password field is a potential security risk as it could turn parts of the password into clickable links, which is inappropriate for sensitive information.

```diff
 <TextView
     android:id="@+id/textLocalPassword"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="start"
     android:layout_margin="4dp"
-    android:autoLink="web"
     tools:text="1q2w3e4r" />
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                <TextView
                    android:id="@+id/textLocalPassword"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="start"
                    android:layout_margin="4dp"
                    tools:text="1q2w3e4r" />
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -4,10 +4,123 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:gravity="bottom"
     android:orientation="vertical"
     tools:context=".ui.HomeFragment">
 
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        app:cardCornerRadius="16dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="4dp">
+
+            <ImageView
+                android:id="@+id/imageServer"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_margin="4dp"
+                android:src="@drawable/ic_server"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <GridLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:columnCount="2"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@id/imageServer"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_columnSpan="2"
+                    android:text="Server"
+                    android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
path: app/src/main/res/layout/fragment_home.xml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use string resource instead of hardcoded text**

For better localization support, extract the hardcoded "Server" text to a string resource.

```diff
 <TextView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_columnSpan="2"
-    android:text="Server"
+    android:text="@string/server"
     android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_columnSpan="2"
                    android:text="@string/server"
                    android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits d087f9c to bac8df3
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+package app.callgate.android.modules.connection.ip
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.request.get
+
+class PublicIPProvider : IPProvider {
+    override suspend fun getIP(): String? {
+        val client = HttpClient(OkHttp) {
+            install(HttpTimeout) {
+                requestTimeoutMillis = 5000
+            }
+        }
+
+        return try {
+            client.get("https://api.ipify.org").body<String>()
+        } catch (e: Exception) {
+            e.printStackTrace()
+            null
+        } finally {
+            client.close()
+        }
+    }
+}
path: app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Optimize HttpClient usage and improve error handling**

Several improvements can be made to this implementation:

1. Creating a new HttpClient for each call is inefficient. Consider making it a class property and initializing it once.
2. The hardcoded URL should be a constant for better maintainability.
3. Replace printStackTrace() with proper logging.
4. Consider more specific exception handling.


```diff
package app.callgate.android.modules.connection.ip

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.get
+import android.util.Log

class PublicIPProvider : IPProvider {
+    companion object {
+        private const val TAG = "PublicIPProvider"
+        private const val PUBLIC_IP_URL = "https://api.ipify.org"
+        private const val REQUEST_TIMEOUT_MS = 5000L
+    }
+    
+    private val client = HttpClient(OkHttp) {
+        install(HttpTimeout) {
+            requestTimeoutMillis = REQUEST_TIMEOUT_MS
+        }
+    }

    override suspend fun getIP(): String? {
-        val client = HttpClient(OkHttp) {
-            install(HttpTimeout) {
-                requestTimeoutMillis = 5000
-            }
-        }

        return try {
-            client.get("https://api.ipify.org").body<String>()
+            client.get(PUBLIC_IP_URL).body<String>()
-        } catch (e: Exception) {
-            e.printStackTrace()
+        } catch (e: Exception) {
+            Log.e(TAG, "Error fetching public IP", e)
            null
-        } finally {
-            client.close()
        }
    }
+    
+    // Ensure resources are released when the provider is no longer needed
+    fun close() {
+        client.close()
+    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
package app.callgate.android.modules.connection.ip

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.get
import android.util.Log

class PublicIPProvider : IPProvider {
    companion object {
        private const val TAG = "PublicIPProvider"
        private const val PUBLIC_IP_URL = "https://api.ipify.org"
        private const val REQUEST_TIMEOUT_MS = 5000L
    }

    private val client = HttpClient(OkHttp) {
        install(HttpTimeout) {
            requestTimeoutMillis = REQUEST_TIMEOUT_MS
        }
    }

    override suspend fun getIP(): String? {
        return try {
            client.get(PUBLIC_IP_URL).body<String>()
        } catch (e: Exception) {
            Log.e(TAG, "Error fetching public IP", e)
            null
        }
    }

    // Ensure resources are released when the provider is no longer needed
    fun close() {
        client.close()
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+package app.callgate.android.modules.connection.ip
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.request.get
+
+class PublicIPProvider : IPProvider {
+    override suspend fun getIP(): String? {
+        val client = HttpClient(OkHttp) {
+            install(HttpTimeout) {
+                requestTimeoutMillis = 5000
+            }
+        }
path: app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider adding network connectivity check**

Before making the network request, check if the device has internet connectivity to avoid unnecessary network calls and provide better user feedback.

---

🌐 Web query:

```web
Best practices for checking network connectivity in Android Kotlin
```

💡 Result:

## Best Practices for Checking Network Connectivity in Android (Kotlin)

Ensuring reliable network connectivity checks is fundamental for robust Android applications, especially those with heavy reliance on internet resources. Here are the best practices for checking network connectivity in Android using Kotlin.

---

**1. Use ConnectivityManager and NetworkCapabilities**

The recommended approach for checking network connectivity is by using the `ConnectivityManager` and, on newer APIs (Android 6.0, API level 23+), `NetworkCapabilities`. This method is more accurate than relying on deprecated APIs.

```kotlin
fun checkForInternet(context: Context): Boolean {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val network = connectivityManager.activeNetwork ?: return false
        val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false
        return activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
               activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
    } else {
        @Suppress("DEPRECATION")
        val networkInfo = connectivityManager.activeNetworkInfo ?: return false
        @Suppress("DEPRECATION")
        return networkInfo.isConnected
    }
}
```
This approach checks if the device is connected via Wi-Fi or cellular transport and is compatible with both modern and legacy devices[6].

---

**2. Register for Real-Time Connectivity Updates**

For dynamic network monitoring, register a callback using `ConnectivityManager` to receive real-time updates about connectivity status. This is essential for apps that need to respond immediately to changes.

- On API level 24+, consider using a lifecycle-aware observer pattern that emits connectivity state changes as a Flow or LiveData, which is especially clean with Kotlin coroutines[8].
- For older APIs, you may need to use a `BroadcastReceiver` that listens to `CONNECTIVITY_ACTION` intents, though this is deprecated for apps targeting Android N and above[1].

---

**3. Always Check Before User-Initiated Network Requests**

Check connectivity **immediately before** performing any user-initiated network requests. This prevents unnecessary attempts and battery drain caused by activating mobile radios[2]. Always fail gracefully and inform the user if no connection is available.

---

**4. Use Appropriate Permissions**

Your app must request the following permissions in the manifest:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
These are necessary for network access and for querying the connection state[9].

---

**5. Handle Null Safety and Edge Cases**

- Use Kotlin's null safety features to handle potentially null network objects, reducing the risk of `NullPointerException`[4].
- Consider all possible transport types (Wi-Fi, cellular, Ethernet, etc.) to make your connectivity check more robust.

---

**6. Observe Not Just Connectivity, But Internet Availability**

Being connected to a network does not guarantee internet access. For critical operations, you may want to perform a small ping or lightweight request to verify actual internet reachability[3].

---

**7. Structure Checks for Maintainability**

- Abstract network checks into a helper class or repository for testability and re-use[8].
- Use coroutines for asynchronous, non-blocking execution, especially when performing additional checks like pinging a server[4].

---

**Example: Modern Connectivity Observer Pattern**

You can implement a connectivity observer that emits network status via Kotlin Flows or LiveData, providing lifecycle-aware updates throughout your app[8]:

```kotlin
interface ConnectivityObserver {
    fun observe(): Flow<Status>
    enum class Status { Available, Unavailable, Losing, Lost }
}
```
This approach decouples the connectivity logic and makes it easily testable and reusable.

---

**Summary Table: Key Recommendations**

| Practice                                 | Rationale                                 |
|-------------------------------------------|-------------------------------------------|
| Use ConnectivityManager + NetworkCapabilities | Accurate, future-proof API                |
| Register for connectivity updates         | Real-time, lifecycle-aware status         |
| Check connectivity before requests        | Battery and UX optimization               |
| Request necessary permissions             | Required by Android for network APIs      |
| Handle null and edge cases                | Prevent crashes and missed connections    |
| Verify actual internet access as needed   | Ensures reliability for critical flows    |
| Use coroutines for background checks      | Non-blocking, better performance          |

---

Applying these best practices will help your Android Kotlin app reliably detect and respond to network connectivity changes, improving user experience and app stability[1][2][6][8].

Citations:

- 1: https://developer.android.com/develop/connectivity/network-ops/managing
- 2: https://developer.android.com/develop/connectivity/network-ops/network-access-optimization
- 3: https://www.youtube.com/watch?v=wvDPG2iQ-OE
- 4: https://www.bluecoding.com/post/mastering-kotlin-for-android-development-10-best-practices
- 5: https://www.securew2.com/blog/2024-guide-to-android-network-settings
- 6: https://www.geeksforgeeks.org/how-to-check-internet-connection-in-kotlin/
- 7: https://source.android.com/docs/security/best-practices/network
- 8: https://www.youtube.com/watch?v=TzV0oCRDNfM
- 9: https://developer.android.com/develop/connectivity/network-ops/connecting
- 10: https://developer.android.com/training/monitoring-device-state/connectivity-status-type

---



</details>

**Add a network‐connectivity check before HTTP calls**

Before instantiating the `HttpClient`, verify that the device is actually online using `ConnectivityManager` + `NetworkCapabilities` (or the legacy `activeNetworkInfo` on API < 23). This avoids unnecessary timeouts and lets you fail fast with a user‐friendly message.

• File: `app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt`  
  - Introduce a helper:
    ```kotlin
    private fun hasInternet(context: Context): Boolean {
      val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
            || caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
      } else {
        @Suppress("DEPRECATION")
        return cm.activeNetworkInfo?.isConnected == true
      }
    }
    ```
  - In your network method (e.g. `getPublicIP()`), do:
    ```kotlin
    if (!hasInternet(context)) {
      throw NoInternetException("No network connection")
    }
    val client = HttpClient(OkHttp) { … }
    ```
• Manifest: Ensure you have  
  ```xml
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  ```

This will give immediate feedback when offline and prevent wasted network attempts.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+package app.callgate.android.modules.connection.ip
+
+import android.content.Context
+import android.net.wifi.WifiManager
+import java.net.Inet4Address
+import java.net.NetworkInterface
+
+
+class LocalIPProvider(private val context: Context) : IPProvider {
+    override suspend fun getIP(): String? {
+        try {
+            val wifiManager =
+                context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
+
+            // If the device is in tethering mode, the following method may return null or a default IP
+            wifiManager.connectionInfo?.let { connectionInfo ->
+                val ipInt = connectionInfo.ipAddress
+                if (ipInt != 0) {
+                    return (ipInt and 0xFF).toString() + "." +
+                            ((ipInt shr 8) and 0xFF) + "." +
+                            ((ipInt shr 16) and 0xFF) + "." +
+                            ((ipInt shr 24) and 0xFF)
+                }
+            }
+
+            // If the above doesn't work, try to find the WiFi network interface directly
+            val wifiInterface = NetworkInterface.getNetworkInterfaces().asSequence()
+                .find { it.name.contains("wlan", ignoreCase = true) }
+
+            wifiInterface?.inetAddresses?.asSequence()
+                ?.find { !it.isLoopbackAddress && it is Inet4Address }
+                ?.let { inetAddress ->
+                    return inetAddress.hostAddress
+                }
+
+            // Check any other network interface
+            val interfaces = NetworkInterface.getNetworkInterfaces()
+            while (interfaces.hasMoreElements()) {
+                val intf = interfaces.nextElement()
+                val addrs = intf.inetAddresses
+                while (addrs.hasMoreElements()) {
+                    val addr = addrs.nextElement()
+                    if (!addr.isLoopbackAddress && addr is Inet4Address) {
+                        return addr.hostAddress
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
path: app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling with proper logging**

Using `e.printStackTrace()` for error handling is not ideal for production code. Consider using a proper logging framework or at least providing more context about what failed.


```diff
    } catch (e: Exception) {
-        e.printStackTrace()
+        Log.e("LocalIPProvider", "Failed to retrieve local IP address", e)
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        } catch (e: Exception) {
            Log.e("LocalIPProvider", "Failed to retrieve local IP address", e)
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,30 @@
+package app.callgate.android.modules.connection
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ConnectionViewModel(
+    private val connectionSvc: ConnectionService
+) : ViewModel() {
+    private val _localIP = MutableLiveData<String>()
+    val localIP: LiveData<String> = _localIP
+
+    private val _publicIP = MutableLiveData<String>()
+    val publicIP: LiveData<String> = _publicIP
+
+    fun getLocalIP() {
+        viewModelScope.launch(Dispatchers.IO) {
+            _localIP.postValue(connectionSvc.getLocalIP())
+        }
+    }
+
+    fun getPublicIP() {
+        viewModelScope.launch(Dispatchers.IO) {
+            _publicIP.postValue(connectionSvc.getPublicIP())
+        }
+    }
path: app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for public IP retrieval**

Similar to the local IP retrieval, the public IP retrieval also lacks error handling. Public IP fetching involves network requests that are more likely to fail.


```diff
fun getPublicIP() {
    viewModelScope.launch(Dispatchers.IO) {
-        _publicIP.postValue(connectionSvc.getPublicIP())
+        try {
+            _publicIP.postValue(connectionSvc.getPublicIP())
+        } catch (e: Exception) {
+            Log.e("ConnectionViewModel", "Failed to get public IP", e)
+            _publicIP.postValue(null)
+        }
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    fun getPublicIP() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _publicIP.postValue(connectionSvc.getPublicIP())
            } catch (e: Exception) {
                Log.e("ConnectionViewModel", "Failed to get public IP", e)
                _publicIP.postValue(null)
            }
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,30 @@
+package app.callgate.android.modules.connection
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ConnectionViewModel(
+    private val connectionSvc: ConnectionService
+) : ViewModel() {
+    private val _localIP = MutableLiveData<String>()
+    val localIP: LiveData<String> = _localIP
+
+    private val _publicIP = MutableLiveData<String>()
+    val publicIP: LiveData<String> = _publicIP
+
+    fun getLocalIP() {
+        viewModelScope.launch(Dispatchers.IO) {
+            _localIP.postValue(connectionSvc.getLocalIP())
+        }
+    }
path: app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for IP retrieval**

The current implementation doesn't handle potential errors when retrieving the local IP. If the connection service throws an exception, it will be swallowed by the coroutine with no feedback to the user.


```diff
fun getLocalIP() {
    viewModelScope.launch(Dispatchers.IO) {
-        _localIP.postValue(connectionSvc.getLocalIP())
+        try {
+            _localIP.postValue(connectionSvc.getLocalIP())
+        } catch (e: Exception) {
+            Log.e("ConnectionViewModel", "Failed to get local IP", e)
+            _localIP.postValue(null)
+        }
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    fun getLocalIP() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _localIP.postValue(connectionSvc.getLocalIP())
            } catch (e: Exception) {
                Log.e("ConnectionViewModel", "Failed to get local IP", e)
                _localIP.postValue(null)
            }
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:18Z
author_association: NONE
diff_hunk: @@ -108,6 +145,40 @@ class HomeFragment : Fragment() {
         }
     }
 
+    private fun TextView.setCopyableText(text: String) {
+        val source = Html.fromHtml("<a href>$text</a>")
+        val builder = SpannableStringBuilder(source)
+        val spans = builder.getSpans(0, builder.length, URLSpan::class.java)
+        for (span in spans) {
+            val innerText = builder.subSequence(
+                builder.getSpanStart(span),
+                builder.getSpanEnd(span)
+            ).toString()
+            val clickableSpan = object : ClickableSpan() {
+
+                override fun onClick(widget: View) {
+                    val clipboard = requireContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE
+                    ) as ClipboardManager
+                    clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
+                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
+                        Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
+                            .show()
+                }
+            }
+            builder.setSpan(
+                clickableSpan,
+                builder.getSpanStart(span),
+                builder.getSpanEnd(span),
+                builder.getSpanFlags(span)
+            )
+            builder.removeSpan(span)
+        }
+
+        this.movementMethod = LinkMovementMethod.getInstance()
+        this.text = builder.toSpanned()
+    }
path: app/src/main/java/app/callgate/android/ui/HomeFragment.kt
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Simplify the setCopyableText extension function**

The current implementation uses HTML parsing and SpannableStringBuilder which is quite complex for making text copyable. Consider a simpler approach using OnClickListener directly.


```diff
-private fun TextView.setCopyableText(text: String) {
-    val source = Html.fromHtml("<a href>$text</a>")
-    val builder = SpannableStringBuilder(source)
-    val spans = builder.getSpans(0, builder.length, URLSpan::class.java)
-    for (span in spans) {
-        val innerText = builder.subSequence(
-            builder.getSpanStart(span),
-            builder.getSpanEnd(span)
-        ).toString()
-        val clickableSpan = object : ClickableSpan() {
-
-            override fun onClick(widget: View) {
-                val clipboard = requireContext().getSystemService(
-                    Context.CLIPBOARD_SERVICE
-                ) as ClipboardManager
-                clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
-                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
-                    Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
-                        .show()
-            }
-        }
-        builder.setSpan(
-            clickableSpan,
-            builder.getSpanStart(span),
-            builder.getSpanEnd(span),
-            builder.getSpanFlags(span)
-        )
-        builder.removeSpan(span)
-    }
-
-    this.movementMethod = LinkMovementMethod.getInstance()
-    this.text = builder.toSpanned()
-}
+private fun TextView.setCopyableText(text: String) {
+    this.text = text
+    this.setOnClickListener {
+        try {
+            val clipboard = requireContext().getSystemService(
+                Context.CLIPBOARD_SERVICE
+            ) as ClipboardManager
+            clipboard.setPrimaryClip(ClipData.newPlainText("", text))
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+                Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+            }
+        } catch (e: Exception) {
+            Log.e("HomeFragment", "Failed to copy text to clipboard", e)
+            Toast.makeText(context, R.string.copy_failed, Toast.LENGTH_SHORT).show()
+        }
+    }
+    
+    // Make it visually indicate it's clickable
+    this.setTextColor(ContextCompat.getColor(context, R.color.link_color))
+}
```

Note: You'll need to:
1. Add a string resource for copy_failed
2. Add a color resource for link_color or use an existing one

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if R.color.link_color exists
grep -r "name=\"link_color\"" app/src/main/res/values/ || echo "link_color resource not found"
```

Length of output: 123

---



</details>

**Simplify setCopyableText and add missing resources**

The custom HTML‑parsing + Spannable logic can be replaced with a simple click listener. You’ll also need to define two new resources that aren’t present:

• In HomeFragment.kt, replace your extension with:
```kotlin
private fun TextView.setCopyableText(text: String) {
  this.text = text
  setOnClickListener {
    try {
      val clipboard = requireContext().getSystemService(
        Context.CLIPBOARD_SERVICE
      ) as ClipboardManager
      clipboard.setPrimaryClip(ClipData.newPlainText("", text))
      if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
        Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
      }
    } catch (e: Exception) {
      Log.e("HomeFragment", "Failed to copy text", e)
      Toast.makeText(context, R.string.copy_failed, Toast.LENGTH_SHORT).show()
    }
  }
  // visually indicate clickability
  setTextColor(ContextCompat.getColor(context, R.color.link_color))
}
```

• Add to res/values/strings.xml:
```xml
<string name="copy_failed">Failed to copy text</string>
```

• Add to res/values/colors.xml:
```xml
<color name="link_color">#0066CC</color>  <!-- or your existing link color -->
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:18Z
author_association: NONE
diff_hunk: @@ -108,6 +145,40 @@ class HomeFragment : Fragment() {
         }
     }
 
+    private fun TextView.setCopyableText(text: String) {
+        val source = Html.fromHtml("<a href>$text</a>")
+        val builder = SpannableStringBuilder(source)
+        val spans = builder.getSpans(0, builder.length, URLSpan::class.java)
+        for (span in spans) {
+            val innerText = builder.subSequence(
+                builder.getSpanStart(span),
+                builder.getSpanEnd(span)
+            ).toString()
+            val clickableSpan = object : ClickableSpan() {
+
+                override fun onClick(widget: View) {
+                    val clipboard = requireContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE
+                    ) as ClipboardManager
+                    clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
+                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
+                        Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
+                            .show()
+                }
path: app/src/main/java/app/callgate/android/ui/HomeFragment.kt
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for clipboard operations**

The clipboard operation might throw SecurityException on some devices. Wrap it in a try-catch block to handle potential errors.


```diff
override fun onClick(widget: View) {
-    val clipboard = requireContext().getSystemService(
-        Context.CLIPBOARD_SERVICE
-    ) as ClipboardManager
-    clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
-    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
-        Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
-            .show()
+    try {
+        val clipboard = requireContext().getSystemService(
+            Context.CLIPBOARD_SERVICE
+        ) as ClipboardManager
+        clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+        }
+    } catch (e: Exception) {
+        Log.e("HomeFragment", "Failed to copy text to clipboard", e)
+        Toast.makeText(context, R.string.copy_failed, Toast.LENGTH_SHORT).show()
+    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                override fun onClick(widget: View) {
+                   try {
                        val clipboard = requireContext().getSystemService(
                            Context.CLIPBOARD_SERVICE
                        ) as ClipboardManager
                        clipboard.setPrimaryClip(ClipData.newPlainText("", innerText))
                        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
                            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
                                .show()
                        }
+                   } catch (e: Exception) {
+                       Log.e("HomeFragment", "Failed to copy text to clipboard", e)
+                       Toast.makeText(context, R.string.copy_failed, Toast.LENGTH_SHORT)
+                           .show()
+                   }
                }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T10:56:18Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-18T10:56:14Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a new "connection" module into the Android application, providing functionality to retrieve and display both local and public IP addresses. The changes include new service and ViewModel classes for connection management, integration of these components into the app's dependency injection setup, and UI enhancements in the HomeFragment to show the IP addresses and allow users to copy them to the clipboard. The AndroidManifest is updated to request Wi-Fi state access, and new string resources and layout elements were added to support the feature. Additionally, the server module is extended with a new ServerViewModel exposing server credentials, the ServerService's settings property is made publicly accessible, navigation components are integrated, and settings navigation is improved.

## Changes

| File(s)                                                                                                         | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
|-----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `app/src/main/AndroidManifest.xml`                                                                              | Added the `android.permission.ACCESS_WIFI_STATE` permission to allow access to Wi-Fi network information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `app/src/main/java/app/callgate/android/App.kt`                                                                 | Imported and included the new `connectionModule` in the Koin dependency injection setup during application initialization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt`                                | Introduced `ConnectionService` class for retrieving local and public IP addresses asynchronously using `LocalIPProvider` and `PublicIPProvider`.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt`                              | Added `ConnectionViewModel` class extending `ViewModel`, exposing LiveData for local and public IPs, and providing methods to fetch them asynchronously via `ConnectionService`.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `app/src/main/java/app/callgate/android/modules/connection/Module.kt`                                           | Created a Koin module (`connectionModule`) registering `ConnectionService` and `ConnectionViewModel` for dependency injection; declared a module name constant.                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt`                                    | Introduced `IPProvider` interface with a suspending function to retrieve an IP address.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt`                               | Implemented `LocalIPProvider` class to obtain the device's local IPv4 address using Wi-Fi manager and network interfaces, handling multiple strategies and exceptions.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt`                              | Implemented `PublicIPProvider` class to fetch the device's public IP address via HTTP request to an external service, handling exceptions and resource cleanup.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `app/src/main/java/app/callgate/android/modules/server/ServerService.kt`                                        | Changed the visibility of the `settings` property in `ServerService` from private to public, allowing external access.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `app/src/main/java/app/callgate/android/modules/server/Module.kt`                                               | Added `ServerViewModel` binding to the server module for dependency injection.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt`                                      | Added `ServerViewModel` class exposing server credentials as LiveData and providing a refresh method to update credentials from `ServerService`, including a nested `Credentials` data class.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `app/src/main/java/app/callgate/android/ui/HomeFragment.kt`                                                     | Enhanced `HomeFragment` to observe and display local/public IPs from `ConnectionViewModel` and server credentials from `ServerViewModel`; added clipboard copy feature for IPs and credentials; updated lifecycle methods to fetch IPs and refresh credentials on resume; added a private extension function for copyable text in TextViews; changed settings navigation to use Android Navigation Component.                                                                                                                                                                                                                 |
| `app/src/main/res/layout/fragment_home.xml`                                                                     | Modified layout: removed gravity attribute, added a new section with a `CardView` and `GridLayout` to display server info including local/public IPs, username, and password; preserved existing buttons.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `app/src/main/res/values/strings.xml`                                                                           | Added new string resources: "local_address", "dots", "copied_to_clipboard", "failed_to_copy_to_clipboard", and "n_a" for use in the updated UI.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `app/src/main/java/app/callgate/android/MainActivity.kt`                                                        | Replaced manual fragment transaction with Android Navigation Component setup using `NavHostFragment` and `NavController`; configured toolbar to work with navigation.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `app/src/main/res/layout/activity_main.xml`                                                                     | Changed root layout to vertical `LinearLayout` including a `MaterialToolbar` and a `FragmentContainerView` configured as a navigation host fragment with navigation graph and default host settings.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `app/src/main/res/navigation/nav_graph.xml`                                                                     | Added a navigation graph defining fragments for home, settings, server settings, and webhooks settings with navigation actions and animations.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `app/src/main/res/menu/nav_menu.xml`                                                                             | Added an empty menu resource file for navigation menu.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt`                                         | Added click listeners to preferences with keys `"navigation.server"` and `"navigation.webhooks"` to navigate to corresponding settings fragments; removed companion object with `newInstance()` method.                                                                                                                                                                                                                                                                                                                                                                                                       |
| `app/src/main/res/xml/root_preferences.xml`                                                                      | Added `app:key` attributes `"navigation.server"` and `"navigation.webhooks"` to server and webhook preference elements respectively.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `app/build.gradle.kts`                                                                                           | Added AndroidX Navigation dependencies (`navigation-fragment-ktx` and `navigation-ui-ktx`) with version "2.8.9".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt`                                   | Added a blank line after imports; no functional changes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant HomeFragment
    participant ConnectionViewModel
    participant ConnectionService
    participant LocalIPProvider
    participant PublicIPProvider

    User->>HomeFragment: Opens HomeFragment (onResume)
    HomeFragment->>ConnectionViewModel: getLocalIP()
    HomeFragment->>ConnectionViewModel: getPublicIP()
    ConnectionViewModel->>ConnectionService: getLocalIP()
    ConnectionService->>LocalIPProvider: getIP()
    LocalIPProvider-->>ConnectionService: Local IP String?
    ConnectionService-->>ConnectionViewModel: Local IP String?
    ConnectionViewModel-->>HomeFragment: Update LiveData (localIP)
    ConnectionViewModel->>ConnectionService: getPublicIP()
    ConnectionService->>PublicIPProvider: getIP()
    PublicIPProvider-->>ConnectionService: Public IP String?
    ConnectionService-->>ConnectionViewModel: Public IP String?
    ConnectionViewModel-->>HomeFragment: Update LiveData (publicIP)
    HomeFragment->>User: Displays IPs in UI (copyable)
    User->>HomeFragment: Taps IP address
    HomeFragment->>User: Copies IP to clipboard, shows Toast
```

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 47d36bda288604acb73adf03f70bdc5140fab45e and 189b17c0ba1a3cf737fd5caae0e174a4468c8fed.

</details>

<details>
<summary>📒 Files selected for processing (22)</summary>

* `app/build.gradle.kts` (1 hunks)
* `app/src/main/AndroidManifest.xml` (1 hunks)
* `app/src/main/java/app/callgate/android/App.kt` (2 hunks)
* `app/src/main/java/app/callgate/android/MainActivity.kt` (2 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/Module.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/Module.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/ServerService.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/ui/HomeFragment.kt` (4 hunks)
* `app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt` (1 hunks)
* `app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt` (2 hunks)
* `app/src/main/res/layout/activity_main.xml` (1 hunks)
* `app/src/main/res/layout/fragment_home.xml` (1 hunks)
* `app/src/main/res/menu/nav_menu.xml` (1 hunks)
* `app/src/main/res/navigation/nav_graph.xml` (1 hunks)
* `app/src/main/res/values/strings.xml` (1 hunks)
* `app/src/main/res/xml/root_preferences.xml` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* app/src/main/java/app/callgate/android/ui/settings/BasePreferenceFragment.kt
* app/build.gradle.kts
* app/src/main/res/values/strings.xml

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (19)</summary>

* app/src/main/res/menu/nav_menu.xml
* app/src/main/java/app/callgate/android/modules/server/Module.kt
* app/src/main/res/xml/root_preferences.xml
* app/src/main/java/app/callgate/android/App.kt
* app/src/main/AndroidManifest.xml
* app/src/main/java/app/callgate/android/modules/connection/ip/IPProvider.kt
* app/src/main/java/app/callgate/android/modules/connection/Module.kt
* app/src/main/java/app/callgate/android/MainActivity.kt
* app/src/main/res/navigation/nav_graph.xml
* app/src/main/java/app/callgate/android/ui/settings/SettingsFragment.kt
* app/src/main/java/app/callgate/android/modules/server/ServerService.kt
* app/src/main/res/layout/activity_main.xml
* app/src/main/java/app/callgate/android/modules/connection/ip/PublicIPProvider.kt
* app/src/main/java/app/callgate/android/modules/connection/ip/LocalIPProvider.kt
* app/src/main/res/layout/fragment_home.xml
* app/src/main/java/app/callgate/android/modules/connection/ConnectionViewModel.kt
* app/src/main/java/app/callgate/android/modules/server/ServerViewModel.kt
* app/src/main/java/app/callgate/android/modules/connection/ConnectionService.kt
* app/src/main/java/app/callgate/android/ui/HomeFragment.kt

</details>

</details>
<!-- internal state start -->


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

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=call-gate-app/android-app&utm_content=6):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-21T02:05:39Z
body: ## 🤖 Pull request artifacts

| file | commit |
| ---- | ------ |
| [`app-release.apk`](https://s3.sms-gate.app/call-gate/189b17c0ba1a3cf737fd5caae0e174a4468c8fed/app-release.apk) | 189b17c0ba1a3cf737fd5caae0e174a4468c8fed |
| [`app-release.aab`](https://s3.sms-gate.app/call-gate/189b17c0ba1a3cf737fd5caae0e174a4468c8fed/app-release.aab) | 189b17c0ba1a3cf737fd5caae0e174a4468c8fed |
author_association: NONE