Repository navigation
Implement activities (files versions) #3377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
daniele-verducci
wants to merge
75
commits into
main
Choose a base branch
from
feat/versions-activities
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+5,061
−1,090
Open
Changes from all commits
Commits
Show all changes
75 commits
Select commit
Hold shift + click to select a range
34b28bc
Added menu item
daniele-verducci 5c93ff7
Item detail layout and resources
daniele-verducci e79e0b7
Stubbed activities fragment, implemented ItemDetailsActivity with pag…
daniele-verducci 20a7b2a
Replaced menu items
daniele-verducci 8ef2e7b
Manifest
daniele-verducci 13df6bb
wip
daniele-verducci 83c9578
Converted NoteShareActivity.java to Kotlin (automated conversion, no …
daniele-verducci aaa0afb
Refactored NoteShareActivity to be more kotlin idiomatic
daniele-verducci 7bb54db
Converted NoteShareActivity to fragment
daniele-verducci 0c739bd
Removed stale code
daniele-verducci 5eae9ba
Cleanup: the searchable <meta-data> wasn't present in the activity de…
daniele-verducci aaf55bb
WIP passing required parameters
daniele-verducci c3bf76e
Wired new details screen in place of old share, preserved system shar…
daniele-verducci 283510b
Moved collectNoteContents to worker thread to fix lint error (already…
daniele-verducci e8b755a
Fixed view name
daniele-verducci bf0c1fa
Header content, tabs icons, theming
daniele-verducci 03e5773
Actionbar
daniele-verducci 55cdf7f
Removed size (we don't have such info right now)
daniele-verducci f53d63f
Stubbed activities API and repository
daniele-verducci 2a6f7b3
Missing copyright headers
daniele-verducci 003937e
Activities list layout
daniele-verducci fe13e5c
Implemented activities APIs
daniele-verducci e938488
Implemented pagination for activities API
daniele-verducci 2c47298
Implemented activities list, fix activity optional fields
daniele-verducci 12a0f00
Fix
daniele-verducci a83711d
Empty list
daniele-verducci b2ff5fa
Pull-to-refresh
daniele-verducci 92b3d26
List incremental loading
daniele-verducci b3062f1
WIP icons in activities list
daniele-verducci 3a32b76
Prevent Glide to rewrite the url and point to the WebDAV storage for …
daniele-verducci faa55ae
Converted CustomAppGlideModule to kotlin
daniele-verducci dd7da4b
More kotlin-idiomatic CustomAppGlideModule
daniele-verducci 2f1a1f9
Added SVG transcoder to CustomAppGlideModule (note: added dependency …
daniele-verducci 46ed282
Added missing SvgDecoder (from Files app)
daniele-verducci 3844c61
Fixed Glide module not loaded since it was converted to Kotlin few co…
daniele-verducci 645d14f
gradlew --write-verification-metadata
daniele-verducci c437b94
Implemented Versions API (note: added dependency jackrabbit-webdav, a…
daniele-verducci 9e5103e
Implemented Versions repo
daniele-verducci 6b9d13d
Fixed Versions call failing because unauthenticated
daniele-verducci c338097
Implemented Activity/Versions ordered merging and updated adapter to …
daniele-verducci d8cdf04
Fixed activity pagination (fileVersion endpoint donesn't have paginat…
daniele-verducci 261e9f2
Applied branding color
daniele-verducci cdcaebe
List loading/refresh
daniele-verducci 741424b
Restore version action icon
daniele-verducci 5aab38d
Implemented version restore, but MOVE Dav method is blocked by SSO li…
daniele-verducci 49ccd7f
Updated MOVE WebDAV request call to SSO lib, user-readable error in c…
daniele-verducci a395ce1
Refactored entire data fetch and presentation
daniele-verducci 0d5b22a
Fixes
daniele-verducci f11a7be
Same order as web version, fixed pagination
daniele-verducci 03ee20f
Unnecessary lateinit
daniele-verducci 7b66b98
Edge case fix + restored missing error message
daniele-verducci 394e966
Prevent empty calls to activities
daniele-verducci 6ca16f1
Fixed empty list on servers with no Activities app
daniele-verducci ff3d4d6
Dates in list
daniele-verducci 7273035
Cleaner repo impl, fixed load loop
daniele-verducci d759097
Catch exception
daniele-verducci 4d5b7a1
Fix wrong page loaded
daniele-verducci f563a00
Close req
daniele-verducci dc1f64c
3-dots button in list alighed to Files app
daniele-verducci aeaa83e
Added empty bottom sheet
daniele-verducci fd061e1
Init bottom sheet layout
daniele-verducci c77de5e
Moved file to its own package
daniele-verducci a594c77
Completed bottom sheet implementation
daniele-verducci ebc1284
Branding, aligned info screen to Files'
daniele-verducci b1f4598
Fix crash
daniele-verducci f495f9d
Safer context
daniele-verducci 0623706
Added missing headers
daniele-verducci 5ce2cc8
Format file sizes
daniele-verducci 6608667
Fix
daniele-verducci d46d7bf
Hide info button when more than one element is selected
daniele-verducci 17c02b5
Sync note after version restore and notify editor show updated note w…
daniele-verducci abc4692
Updated SSO library to a version supporting MOVE WebDAV method
daniele-verducci e709189
Fix verification-metadata
daniele-verducci 64763fe
Avoid creating a Calendar instance for every item
daniele-verducci eb76c7a
Cast
daniele-verducci File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /* | ||
| * Nextcloud Notes - Android Client | ||
| * | ||
| * SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com> | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
| package it.niedermann.owncloud.notes.activities | ||
|
|
||
| import android.content.Context | ||
| import android.util.Log | ||
| import androidx.lifecycle.LifecycleCoroutineScope | ||
| import com.nextcloud.android.sso.helper.SingleAccountHelper | ||
| import it.niedermann.owncloud.notes.activities.ActivitiesFragment.Companion.TAG | ||
| import it.niedermann.owncloud.notes.activities.adapter.ActivitiesData | ||
| import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository | ||
| import it.niedermann.owncloud.notes.activities.repository.ActivitiesResult | ||
| import it.niedermann.owncloud.notes.activities.repository.VersionsRepository | ||
| import it.niedermann.owncloud.notes.activities.repository.VersionsResult | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.async | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.withContext | ||
|
|
||
| class ActivitiesDataProvider ( | ||
| val context: Context, | ||
| val lifecycleScope: LifecycleCoroutineScope, | ||
| val noteId: Long, | ||
| val listener: OnPageFetchedListener | ||
| ) { | ||
| val data = ActivitiesData() | ||
| private var job: Job? = null | ||
|
|
||
| private val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context) | ||
| private val activitiesRepo = ActivitiesRepository(context, ssoAccount) | ||
| private val versionsRepo = VersionsRepository(context, ssoAccount) | ||
|
|
||
| fun fetchFirstPage() { | ||
| job?.cancel() | ||
| job = lifecycleScope.launch { | ||
| val (activitiesResult, versionsResult) = withContext(Dispatchers.IO) { | ||
| val activities = async { | ||
| activitiesRepo.fetchActivities(noteId) | ||
| } | ||
| val versions = async { | ||
| versionsRepo.fetchVersions(noteId) | ||
| } | ||
| activities.await() to versions.await() | ||
| } | ||
|
|
||
| if (!parseActivitiesResult(activitiesResult)) return@launch | ||
|
|
||
| if (!parseVersionsResult(versionsResult)) return@launch | ||
|
|
||
| listener.onPageFetched() | ||
| } | ||
| } | ||
|
|
||
| fun fetchNextPage() { | ||
| if (job?.isActive == true) return | ||
|
|
||
| if (!data.activitiesMore) return | ||
|
|
||
| job = lifecycleScope.launch { | ||
| val activitiesResult = withContext(Dispatchers.IO) { | ||
| activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) | ||
| } | ||
|
|
||
| if (!parseActivitiesResult(activitiesResult)) return@launch | ||
|
|
||
| listener.onPageFetched() | ||
| } | ||
| } | ||
|
|
||
| private fun parseActivitiesResult(activitiesResult: ActivitiesResult): Boolean { | ||
| return when (activitiesResult) { | ||
| is ActivitiesResult.Success -> { | ||
| data.activitiesLastGiven = activitiesResult.lastGiven | ||
| data.activitiesMore = activitiesResult.lastGiven != null | ||
| data.activities.addAll(activitiesResult.activities) | ||
| true | ||
| } | ||
|
|
||
| is ActivitiesResult.FailedNoCapability -> { | ||
| Log.d(TAG, "Server has no Activity endpoint") | ||
| data.activitiesMore = false | ||
| true | ||
| } | ||
|
|
||
| is ActivitiesResult.Failed -> { | ||
| Log.e(TAG, "Fetching activities failed") | ||
| listener.onPageFetchError() | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun parseVersionsResult(versionsResult: VersionsResult): Boolean { | ||
| return when (versionsResult) { | ||
| is VersionsResult.Success -> { | ||
| data.versions.addAll(versionsResult.versions) | ||
| true | ||
| } | ||
| is VersionsResult.Failed -> { | ||
| Log.e(TAG, "Fetching versions failed") | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fun reset() { | ||
| data.activities.clear() | ||
| data.versions.clear() | ||
| data.activitiesLastGiven = null | ||
| data.activitiesMore = true | ||
| } | ||
|
|
||
| interface OnPageFetchedListener { | ||
| fun onPageFetched() | ||
| fun onPageFetchError() | ||
| } | ||
| } |
180 changes: 180 additions & 0 deletions
180
app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| package it.niedermann.owncloud.notes.activities | ||
|
|
||
| import android.annotation.SuppressLint | ||
| import android.os.Bundle | ||
| import android.util.Log | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.annotation.StringRes | ||
| import androidx.lifecycle.lifecycleScope | ||
| import com.google.android.material.snackbar.Snackbar | ||
| import com.nextcloud.android.sso.helper.SingleAccountHelper | ||
| import com.owncloud.android.lib.resources.files.model.FileVersion | ||
| import it.niedermann.owncloud.notes.R | ||
| import it.niedermann.owncloud.notes.activities.adapter.ActivityListAdapter | ||
| import it.niedermann.owncloud.notes.activities.adapter.OnActivityClickListener | ||
| import it.niedermann.owncloud.notes.activities.model.Activity | ||
| import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository | ||
| import it.niedermann.owncloud.notes.activities.repository.RestoreVersionsResult | ||
| import it.niedermann.owncloud.notes.activities.repository.VersionsRepository | ||
| import it.niedermann.owncloud.notes.branding.BrandedFragment | ||
| import it.niedermann.owncloud.notes.branding.BrandedSnackbar | ||
| import it.niedermann.owncloud.notes.branding.BrandingUtil | ||
| import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding | ||
| import it.niedermann.owncloud.notes.persistence.NotesRepository | ||
| import it.niedermann.owncloud.notes.persistence.entity.Account | ||
| import it.niedermann.owncloud.notes.persistence.entity.Note | ||
| import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.withContext | ||
|
|
||
| /* | ||
| * Nextcloud Notes - Android Client | ||
| * | ||
| * SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com> | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
| class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, ActivitiesDataProvider.OnPageFetchedListener { | ||
|
|
||
| lateinit var binding : FragmentActivitiesBinding | ||
| private lateinit var note: Note | ||
| private lateinit var account: Account | ||
| private lateinit var adapter: ActivityListAdapter | ||
|
|
||
| private lateinit var activityDataProvider: ActivitiesDataProvider | ||
|
|
||
| override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | ||
| adapter = ActivityListAdapter(requireContext(), this) | ||
| binding = FragmentActivitiesBinding.inflate(inflater, container, false) | ||
|
|
||
| initializeArguments() | ||
| initializeList() | ||
|
|
||
| return binding.root | ||
| } | ||
|
|
||
| private fun initializeArguments() { | ||
| val bundler = arguments | ||
| note = bundler.getSerializableArgument(ARG_NOTE, Note::class.java) | ||
| ?: throw IllegalArgumentException("Argument $ARG_NOTE is mandatory") | ||
| account = bundler.getSerializableArgument(ARG_ACCOUNT, Account::class.java) | ||
| ?: throw IllegalArgumentException("Argument $ARG_ACCOUNT is mandatory") | ||
| } | ||
|
|
||
| private fun initializeList() { | ||
| val id = note.remoteId | ||
| if (id == null) { | ||
| Log.e(TAG, "Unable to load list: note has no remoteId") | ||
| return | ||
| } | ||
|
|
||
| activityDataProvider = ActivitiesDataProvider( | ||
| requireContext(), | ||
| lifecycleScope, | ||
| id, | ||
| this | ||
| ) | ||
| binding.list.adapter = adapter | ||
| binding.swipeContainingList.setOnRefreshListener { | ||
| // Reload list | ||
| activityDataProvider.reset() | ||
| activityDataProvider.fetchFirstPage() | ||
| } | ||
|
|
||
| activityDataProvider.fetchFirstPage() | ||
| } | ||
|
|
||
| override fun applyBrand(color: Int) { | ||
| adapter.applyBrand(color) | ||
| val util = BrandingUtil.of(color, requireContext()) | ||
| util.platform.colorViewBackground(binding.getRoot()) | ||
| util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) | ||
| } | ||
|
|
||
| override fun onActivityItemClick(activity: Activity) { | ||
| // No action | ||
| } | ||
|
|
||
| override fun onVersionItemClick(version: FileVersion) { | ||
| binding.swipeContainingList.isRefreshing = true | ||
| // Restore the version | ||
| val ssoAccount = | ||
| SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) | ||
| val repository = VersionsRepository(requireContext(), ssoAccount) | ||
| lifecycleScope.launch(Dispatchers.IO) { | ||
| val result = repository.restoreVersion(version) | ||
| withContext(Dispatchers.Main) { | ||
| when (result) { | ||
| RestoreVersionsResult.Success -> syncNote() | ||
| RestoreVersionsResult.FailedUnsupported -> showMessage(R.string.version_restore_failed_unsupported) | ||
| else -> showMessage(R.string.version_restore_failed) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun syncNote() { | ||
| // Sync the restored note | ||
| val noteRepo = NotesRepository.getInstance(binding.root.context) | ||
| noteRepo.addCallbackPull(account, { | ||
| // Used in node editor: RESULT_OK means reload the note | ||
| requireActivity().setResult(android.app.Activity.RESULT_OK) | ||
|
|
||
| // Reload list | ||
| activityDataProvider.reset() | ||
| activityDataProvider.fetchFirstPage() | ||
|
|
||
| // Notify user | ||
| showMessage(R.string.version_restored) | ||
| }) | ||
| noteRepo.scheduleSync(account, false) | ||
| } | ||
|
|
||
| private fun showMessage(@StringRes message: Int) { | ||
| BrandedSnackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() | ||
| } | ||
|
|
||
| override fun lastActivityReached() { | ||
| if (adapter.itemCount < ActivitiesRepository.PAGE_SIZE) | ||
| return | ||
| activityDataProvider.fetchNextPage() | ||
| } | ||
|
|
||
| @SuppressLint("NotifyDataSetChanged") | ||
| override fun onPageFetched() { | ||
| binding.swipeContainingList.isRefreshing = false | ||
| binding.emptyList.visibility = View.GONE | ||
| adapter.items.clear() | ||
| adapter.items.addAll( | ||
| activityDataProvider.data.getAggregatedList(requireContext()) | ||
| ) | ||
| adapter.lastActivity = activityDataProvider.data.activities.lastOrNull() | ||
| adapter.notifyDataSetChanged() | ||
| } | ||
|
|
||
| override fun onPageFetchError() { | ||
| binding.swipeContainingList.isRefreshing = false | ||
| if (adapter.items.isEmpty()) binding.emptyList.visibility = View.VISIBLE | ||
| BrandedSnackbar.make(binding.root, R.string.activities_error, Snackbar.LENGTH_SHORT).show() | ||
| } | ||
|
|
||
| companion object { | ||
| val TAG = ActivitiesFragment::class.simpleName | ||
| const val ARG_NOTE = "NOTE" | ||
| const val ARG_ACCOUNT = "ACCOUNT" | ||
|
|
||
| fun newInstance(note: Note, account: Account) : ActivitiesFragment { | ||
| val f = ActivitiesFragment() | ||
|
|
||
| val bundle = Bundle() | ||
| bundle.putSerializable(ARG_NOTE, note) | ||
| bundle.putSerializable(ARG_ACCOUNT, account) | ||
|
|
||
| f.arguments = bundle | ||
| return f | ||
| } | ||
| } | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Nextcloud Notes - Android Client | ||
| * | ||
| * SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com> | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
| package it.niedermann.owncloud.notes.activities.adapter | ||
|
|
||
| import android.content.Context | ||
| import com.owncloud.android.lib.resources.files.model.FileVersion | ||
| import it.niedermann.owncloud.notes.activities.model.Activity | ||
|
|
||
| class ActivitiesData { | ||
| val activities = arrayListOf<Activity>() | ||
| val versions = arrayListOf<FileVersion>() | ||
| var activitiesLastGiven: Long? = null | ||
| var activitiesMore: Boolean = true | ||
|
|
||
| fun getAggregatedList(context: Context) : List<ActivityListItem> { | ||
| val items = arrayListOf<ActivityListItem>() | ||
| items.addAll( | ||
| activities.map { ActivityListItem(context, it) } | ||
| ) | ||
| items.addAll( | ||
| versions.map { ActivityListItem(context, it) } | ||
| ) | ||
| items.sortDescending() | ||
| return items | ||
| } | ||
|
|
||
| fun isEmpty() : Boolean = activities.isEmpty() && versions.isEmpty() | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be done on main thread?