1+ /*
2+ * Nextcloud Notes - Android Client
3+ *
4+ * SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com>
5+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+ * SPDX-License-Identifier: AGPL-3.0-or-later
7+ */
8+ package it.niedermann.owncloud.notes.activities
9+
10+ import android.annotation.SuppressLint
11+ import android.os.Bundle
12+ import android.util.Log
13+ import android.view.LayoutInflater
14+ import android.view.View
15+ import android.view.ViewGroup
16+ import androidx.annotation.StringRes
17+ import androidx.lifecycle.lifecycleScope
18+ import com.google.android.material.snackbar.Snackbar
19+ import com.nextcloud.android.sso.helper.SingleAccountHelper
20+ import com.owncloud.android.lib.resources.files.model.FileVersion
21+ import it.niedermann.owncloud.notes.R
22+ import it.niedermann.owncloud.notes.activities.adapter.ActivityListAdapter
23+ import it.niedermann.owncloud.notes.activities.adapter.OnActivityClickListener
24+ import it.niedermann.owncloud.notes.activities.adapter.model.Activity
25+ import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository
26+ import it.niedermann.owncloud.notes.activities.repository.model.RestoreVersionsResult
27+ import it.niedermann.owncloud.notes.activities.repository.VersionsRepository
28+ import it.niedermann.owncloud.notes.branding.BrandedFragment
29+ import it.niedermann.owncloud.notes.branding.BrandedSnackbar
30+ import it.niedermann.owncloud.notes.branding.BrandingUtil
31+ import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding
32+ import it.niedermann.owncloud.notes.persistence.NotesRepository
33+ import it.niedermann.owncloud.notes.persistence.entity.Account
34+ import it.niedermann.owncloud.notes.persistence.entity.Note
35+ import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument
36+ import kotlinx.coroutines.Dispatchers
37+ import kotlinx.coroutines.launch
38+ import kotlinx.coroutines.withContext
39+
40+ class ActivitiesFragment : BrandedFragment (), OnActivityClickListener, ActivitiesDataProvider.OnPageFetchedListener {
41+
42+ lateinit var binding : FragmentActivitiesBinding
43+ private lateinit var note: Note
44+ private lateinit var account: Account
45+ private lateinit var adapter: ActivityListAdapter
46+
47+ private lateinit var activityDataProvider: ActivitiesDataProvider
48+
49+ override fun onCreateView (inflater : LayoutInflater , container : ViewGroup ? , savedInstanceState : Bundle ? ): View {
50+ adapter = ActivityListAdapter (requireContext(), this )
51+ binding = FragmentActivitiesBinding .inflate(inflater, container, false )
52+
53+ initializeArguments()
54+ initializeList()
55+
56+ return binding.root
57+ }
58+
59+ private fun initializeArguments () {
60+ val bundler = arguments
61+ note = bundler.getSerializableArgument(ARG_NOTE , Note ::class .java)
62+ ? : throw IllegalArgumentException (" Argument $ARG_NOTE is mandatory" )
63+ account = bundler.getSerializableArgument(ARG_ACCOUNT , Account ::class .java)
64+ ? : throw IllegalArgumentException (" Argument $ARG_ACCOUNT is mandatory" )
65+ }
66+
67+ private fun initializeList () {
68+ val id = note.remoteId
69+ if (id == null ) {
70+ Log .e(TAG , " Unable to load list: note has no remoteId" )
71+ return
72+ }
73+
74+ activityDataProvider = ActivitiesDataProvider (
75+ requireContext(),
76+ lifecycleScope,
77+ id,
78+ this
79+ )
80+ binding.list.adapter = adapter
81+ binding.swipeContainingList.setOnRefreshListener {
82+ // Reload list
83+ activityDataProvider.reset()
84+ activityDataProvider.fetchFirstPage()
85+ }
86+
87+ activityDataProvider.fetchFirstPage()
88+ }
89+
90+ override fun applyBrand (color : Int ) {
91+ adapter.applyBrand(color)
92+ val util = BrandingUtil .of(color, requireContext())
93+ util.platform.colorViewBackground(binding.getRoot())
94+ util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList)
95+ }
96+
97+ override fun onActivityItemClick (activity : Activity ) = Unit
98+
99+ override fun onVersionItemClick (version : FileVersion ) {
100+ showProgress(true )
101+ lifecycleScope.launch(Dispatchers .IO ) {
102+ // Restore the version
103+ val ssoAccount =
104+ SingleAccountHelper .getCurrentSingleSignOnAccount(requireContext())
105+ val repository = VersionsRepository (requireContext(), ssoAccount)
106+ val result = repository.restoreVersion(version)
107+ withContext(Dispatchers .Main ) {
108+ when (result) {
109+ RestoreVersionsResult .Success -> syncNote()
110+ RestoreVersionsResult .FailedUnsupported ->
111+ showMessageAndHideProgress(R .string.version_restore_failed_unsupported)
112+ else -> showMessageAndHideProgress(R .string.version_restore_failed)
113+ }
114+ }
115+ }
116+ }
117+
118+ private fun syncNote () {
119+ // Sync the restored note
120+ val noteRepo = NotesRepository .getInstance(binding.root.context)
121+ noteRepo.addCallbackPull(account, {
122+ // Used in node editor: RESULT_OK means reload the note
123+ requireActivity().setResult(android.app.Activity .RESULT_OK )
124+
125+ // Reload list
126+ activityDataProvider.reset()
127+ activityDataProvider.fetchFirstPage()
128+
129+ // Notify user
130+ showMessageAndHideProgress(R .string.version_restored)
131+ })
132+ noteRepo.scheduleSync(account, false )
133+ }
134+
135+ private fun showProgress (show : Boolean ) { binding.swipeContainingList.isRefreshing = show }
136+
137+ private fun showMessageAndHideProgress (@StringRes message : Int ) {
138+ showProgress(false )
139+ BrandedSnackbar .make(binding.root, message, Snackbar .LENGTH_LONG ).show()
140+ }
141+
142+ override fun lastActivityReached () {
143+ if (adapter.itemCount < ActivitiesRepository .PAGE_SIZE )
144+ return
145+ activityDataProvider.fetchNextPage()
146+ }
147+
148+ @SuppressLint(" NotifyDataSetChanged" )
149+ override fun onPageFetched () {
150+ showProgress(false )
151+ binding.emptyList.visibility = View .GONE
152+ adapter.items.clear()
153+ adapter.items.addAll(
154+ activityDataProvider.data.getAggregatedList(requireContext())
155+ )
156+ adapter.lastActivity = activityDataProvider.data.activities.lastOrNull()
157+ adapter.notifyDataSetChanged()
158+ }
159+
160+ override fun onPageFetchError () {
161+ showProgress(false )
162+ if (adapter.items.isEmpty()) binding.emptyList.visibility = View .VISIBLE
163+ BrandedSnackbar .make(binding.root, R .string.activities_error, Snackbar .LENGTH_SHORT ).show()
164+ }
165+
166+ companion object {
167+ val TAG = ActivitiesFragment ::class .simpleName
168+ const val ARG_NOTE = " NOTE"
169+ const val ARG_ACCOUNT = " ACCOUNT"
170+
171+ fun newInstance (note : Note , account : Account ) : ActivitiesFragment = ActivitiesFragment ().apply {
172+ arguments = Bundle ().apply {
173+ putSerializable(ARG_NOTE , note)
174+ putSerializable(ARG_ACCOUNT , account)
175+ }
176+ }
177+ }
178+ }
0 commit comments