AccountListFragment.kt (7176B)
1 /* 2 This file is part of GNUnet. 3 Copyright (C) 2021--2025 GNUnet e.V. 4 5 GNUnet is free software: you can redistribute it and/or modify it 6 under the terms of the GNU Affero General Public License as published 7 by the Free Software Foundation, either version 3 of the License, 8 or (at your option) any later version. 9 10 GNUnet is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 Affero General Public License for more details. 14 15 You should have received a copy of the GNU Affero General Public License 16 along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 SPDX-License-Identifier: AGPL3.0-or-later 19 */ 20 /* 21 * @author t3sserakt 22 * @file GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt 23 */ 24 25 package org.gnunet.gnunetmessenger.ui.account 26 27 import android.os.Bundle 28 import android.util.Log 29 import android.view.LayoutInflater 30 import android.view.View 31 import android.view.ViewGroup 32 import android.widget.Button 33 import android.widget.ProgressBar 34 import android.widget.TextView 35 import androidx.core.view.isGone 36 import androidx.core.view.isVisible 37 import androidx.fragment.app.Fragment 38 import androidx.lifecycle.lifecycleScope 39 import androidx.navigation.fragment.findNavController 40 import androidx.recyclerview.widget.LinearLayoutManager 41 import androidx.recyclerview.widget.RecyclerView 42 import kotlinx.coroutines.Job 43 import kotlinx.coroutines.launch 44 import org.gnunet.gnunetmessenger.MainActivity 45 import org.gnunet.gnunetmessenger.R 46 import org.gnunet.gnunetmessenger.model.ChatAccount 47 import org.gnunet.gnunetmessenger.ui.adapters.AccountAdapter 48 49 class AccountListFragment : Fragment() { 50 51 private lateinit var recycler: RecyclerView 52 private lateinit var createButton: Button 53 private lateinit var loadingIndicator: ProgressBar 54 private lateinit var statusText: TextView 55 private lateinit var adapter: AccountAdapter 56 57 private val accounts = mutableListOf<ChatAccount>() 58 private var refreshCollectorJob: Job? = null 59 60 companion object { 61 private const val TAG = "AccountListFragment" 62 } 63 64 override fun onCreateView( 65 inflater: LayoutInflater, 66 container: ViewGroup?, 67 savedInstanceState: Bundle? 68 ): View { 69 val view = inflater.inflate(R.layout.fragment_account_list, container, false) 70 val activity = requireActivity() as MainActivity 71 72 recycler = view.findViewById(R.id.account_recycler) 73 createButton = view.findViewById(R.id.btn_create_account) 74 loadingIndicator = view.findViewById(R.id.account_loading_indicator) 75 statusText = view.findViewById(R.id.account_status_text) 76 77 adapter = AccountAdapter { selectedAccount -> 78 viewLifecycleOwner.lifecycleScope.launch { 79 try { 80 // Multi-handle: spawn-or-reuse a per-account session. 81 // Old sessions stay live in the background so a lobby 82 // host survives the switch and can complete the pairing 83 // handshake when the joiner arrives. 84 activity.switchToSession(selectedAccount) 85 } catch (t: Throwable) { 86 Log.e(TAG, "switchToSession failed", t) 87 showError(getString(R.string.account_connect_failed)) 88 return@launch 89 } 90 91 val action = 92 AccountListFragmentDirections.actionAccountListFragmentToAccountOverviewFragment( 93 account = selectedAccount 94 ) 95 findNavController().navigate(action) 96 } 97 } 98 99 recycler.layoutManager = LinearLayoutManager(context) 100 recycler.adapter = adapter 101 102 createButton.setOnClickListener { 103 val action = 104 AccountListFragmentDirections.actionAccountListFragmentToCreateAccountFragment() 105 findNavController().navigate(action) 106 } 107 108 showLoading(getString(R.string.connecting_to_gnunet)) 109 110 return view 111 } 112 113 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 114 super.onViewCreated(view, savedInstanceState) 115 116 val activity = requireActivity() as MainActivity 117 118 viewLifecycleOwner.lifecycleScope.launch { 119 try { 120 val handle = activity.awaitInitialDataReady() 121 Log.d(TAG, "Initial refresh received, loading accounts for handle=${handle.pointer}") 122 123 createButton.isEnabled = true 124 reloadAccounts(showLoading = true) 125 126 refreshCollectorJob?.cancel() 127 refreshCollectorJob = viewLifecycleOwner.lifecycleScope.launch { 128 activity.accountRefreshFlow().collect { 129 Log.d(TAG, "Account refresh event received") 130 reloadAccounts(showLoading = false) 131 } 132 } 133 } catch (t: Throwable) { 134 Log.e(TAG, "Failed to initialize account list", t) 135 showError(getString(R.string.gnunet_connection_failed)) 136 } 137 } 138 } 139 140 override fun onDestroyView() { 141 refreshCollectorJob?.cancel() 142 refreshCollectorJob = null 143 super.onDestroyView() 144 } 145 146 private suspend fun reloadAccounts(showLoading: Boolean) { 147 val activity = requireActivity() as MainActivity 148 val gnunetChat = activity.getGnunetChatInstance() 149 val handle = activity.getChatHandle() 150 151 if (showLoading) { 152 showLoading(getString(R.string.loading_accounts)) 153 } 154 155 try { 156 Log.d(TAG, "listAccounts(): handle=${handle.pointer}") 157 val refreshedAccounts = gnunetChat.listAccounts(handle).map { account -> 158 account.key = gnunetChat.getProfileKey(handle) 159 account 160 } 161 162 accounts.clear() 163 accounts.addAll(refreshedAccounts) 164 adapter.submitList(accounts.toList()) 165 166 loadingIndicator.isGone = true 167 createButton.isEnabled = true 168 169 if (accounts.isEmpty()) { 170 recycler.isGone = true 171 statusText.isVisible = true 172 statusText.text = getString(R.string.no_accounts_available) 173 } else { 174 statusText.isGone = true 175 recycler.isVisible = true 176 } 177 } catch (t: Throwable) { 178 Log.e(TAG, "reloadAccounts failed", t) 179 showError(getString(R.string.account_list_load_failed)) 180 } 181 } 182 183 private fun showLoading(message: String) { 184 loadingIndicator.isVisible = true 185 statusText.isVisible = true 186 statusText.text = message 187 recycler.isGone = true 188 createButton.isEnabled = false 189 } 190 191 private fun showError(message: String) { 192 loadingIndicator.isGone = true 193 statusText.isVisible = true 194 statusText.text = message 195 recycler.isGone = true 196 createButton.isEnabled = false 197 } 198 }