Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
4c6b6c0
Added menu item
daniele-verducci Oct 6, 2026
7088e3f
Item detail layout and resources
daniele-verducci Oct 6, 2026
6e4e838
Stubbed activities fragment, implemented ItemDetailsActivity with pag…
daniele-verducci Oct 6, 2026
3270390
Replaced menu items
daniele-verducci Oct 6, 2026
47d997d
Manifest
daniele-verducci Oct 6, 2026
87f592c
wip
daniele-verducci Oct 6, 2026
fe659a3
Converted NoteShareActivity.java to Kotlin (automated conversion, no …
daniele-verducci Oct 12, 2026
0ea3314
Refactored NoteShareActivity to be more kotlin idiomatic
daniele-verducci Oct 12, 2026
c9d7e25
Converted NoteShareActivity to fragment
daniele-verducci Oct 12, 2026
4f2eccf
Removed stale code
daniele-verducci Oct 12, 2026
d861262
Cleanup: the searchable <meta-data> wasn't present in the activity de…
daniele-verducci Oct 12, 2026
46d3fa4
WIP passing required parameters
daniele-verducci Oct 12, 2026
779337d
Wired new details screen in place of old share, preserved system shar…
daniele-verducci Oct 13, 2026
9eea55f
Moved collectNoteContents to worker thread to fix lint error (already…
daniele-verducci Oct 13, 2026
7edb00a
Fixed view name
daniele-verducci Oct 13, 2026
c847a2c
Header content, tabs icons, theming
daniele-verducci Oct 13, 2026
1c431d7
Actionbar
daniele-verducci Oct 13, 2026
8c7225b
Removed size (we don't have such info right now)
daniele-verducci Oct 13, 2026
8431370
Stubbed activities API and repository
daniele-verducci Oct 13, 2026
a7c9b43
Missing copyright headers
daniele-verducci Oct 14, 2026
ab4c2ca
Activities list layout
daniele-verducci Oct 14, 2026
9502ea7
Implemented activities APIs
daniele-verducci Oct 14, 2026
1a61699
Implemented pagination for activities API
daniele-verducci Oct 14, 2026
0aaf829
Implemented activities list, fix activity optional fields
daniele-verducci Oct 14, 2026
09ff745
Fix
daniele-verducci Oct 14, 2026
10c0167
Empty list
daniele-verducci Oct 14, 2026
f6ac0e0
Pull-to-refresh
daniele-verducci Oct 14, 2026
ab20266
List incremental loading
daniele-verducci Oct 14, 2026
6bdc7bf
WIP icons in activities list
daniele-verducci Oct 14, 2026
819217b
Prevent Glide to rewrite the url and point to the WebDAV storage for …
daniele-verducci Oct 15, 2026
4257a82
Converted CustomAppGlideModule to kotlin
daniele-verducci Oct 15, 2026
b599403
More kotlin-idiomatic CustomAppGlideModule
daniele-verducci Oct 15, 2026
0bc43db
Added SVG transcoder to CustomAppGlideModule (note: added dependency …
daniele-verducci Oct 15, 2026
c91ad34
Added missing SvgDecoder (from Files app)
daniele-verducci Oct 15, 2026
b1e6e5a
Fixed Glide module not loaded since it was converted to Kotlin few co…
daniele-verducci Oct 15, 2026
75b8312
Implemented Versions API (note: added dependency jackrabbit-webdav, a…
daniele-verducci Oct 16, 2026
e8f7d58
Implemented Versions repo
daniele-verducci Oct 16, 2026
d1b0008
Fixed Versions call failing because unauthenticated
daniele-verducci Oct 16, 2026
9b9cc44
Implemented Activity/Versions ordered merging and updated adapter to …
daniele-verducci Oct 19, 2026
dc1dc20
Fixed activity pagination (fileVersion endpoint donesn't have paginat…
daniele-verducci Oct 19, 2026
63fb391
Applied branding color
daniele-verducci Oct 19, 2026
12a469b
List loading/refresh
daniele-verducci Oct 19, 2026
5c11837
Restore version action icon
daniele-verducci Oct 19, 2026
7f03153
Implemented version restore, but MOVE Dav method is blocked by SSO li…
daniele-verducci Oct 19, 2026
929274d
Updated MOVE WebDAV request call to SSO lib, user-readable error in c…
daniele-verducci Oct 20, 2026
f21f785
Refactored entire data fetch and presentation
daniele-verducci Oct 20, 2026
fd126d2
Fixes
daniele-verducci Oct 20, 2026
18dbd59
Same order as web version, fixed pagination
daniele-verducci Oct 21, 2026
fc99a40
Unnecessary lateinit
daniele-verducci Oct 21, 2026
c16f12e
Edge case fix + restored missing error message
daniele-verducci Oct 22, 2026
f26198a
Prevent empty calls to activities
daniele-verducci Oct 22, 2026
5a4f3ba
Fixed empty list on servers with no Activities app
daniele-verducci Oct 22, 2026
5dc5ecb
Dates in list
daniele-verducci Oct 22, 2026
ed72d57
Cleaner repo impl, fixed load loop
daniele-verducci Oct 22, 2026
5435c6d
Catch exception
daniele-verducci Oct 22, 2026
0cd246a
Fix wrong page loaded
daniele-verducci Oct 22, 2026
4816f83
Close req
daniele-verducci Oct 22, 2026
58ff913
3-dots button in list alighed to Files app
daniele-verducci Oct 23, 2026
15101d6
Added empty bottom sheet
daniele-verducci Oct 23, 2026
bc18d12
Init bottom sheet layout
daniele-verducci Oct 23, 2026
902e3c7
Moved file to its own package
daniele-verducci Oct 23, 2026
3ce1862
Completed bottom sheet implementation
daniele-verducci Oct 23, 2026
932f0ab
Branding, aligned info screen to Files'
daniele-verducci Oct 23, 2026
4ab6ff2
Fix crash
daniele-verducci Oct 23, 2026
4d5fd64
Safer context
daniele-verducci Oct 23, 2026
1c5e028
Added missing headers
daniele-verducci Oct 23, 2026
eecd4be
Format file sizes
daniele-verducci Oct 23, 2026
5901290
Fix
daniele-verducci Oct 23, 2026
5d5f7a0
Hide info button when more than one element is selected
daniele-verducci Oct 23, 2026
132d4d0
Sync note after version restore and notify editor show updated note w…
daniele-verducci Oct 23, 2026
45438a5
Avoid creating a Calendar instance for every item
daniele-verducci Oct 28, 2026
e3ff849
Cast
daniele-verducci Oct 28, 2026
db54c15
Sealed class for activities list item
daniele-verducci Nov 2, 2026
381c222
Made restore completely asyncronous, fixed spinner showing after fail…
daniele-verducci Nov 2, 2026
4f601e6
Update verification-metadata.xml
daniele-verducci Nov 2, 2026
1ea8183
Fixed copypasted copyright in a new file
daniele-verducci Nov 2, 2026
37ac3e3
Added verification-metadata as imported by Glide's KSP processor
daniele-verducci Nov 2, 2026
a48256c
Fixed copypasted copyright in a new file
daniele-verducci Nov 2, 2026
c888b77
add missing keys
alperozturk96 Nov 3, 2026
867b4be
use files detekt
alperozturk96 Nov 3, 2026
8b4bd10
add bsd-2-clause
alperozturk96 Nov 3, 2026
5211bd0
better inner class name
alperozturk96 Nov 3, 2026
53a295c
align bottom sheet icons
alperozturk96 Nov 3, 2026
6b9e69c
fix codacy
alperozturk96 Nov 3, 2026
343cdea
fix background color
alperozturk96 Nov 3, 2026
82453cb
optimize imports
alperozturk96 Nov 3, 2026
89186e9
move license header to top of the file
alperozturk96 Nov 3, 2026
f48e84e
Converted ActivitiesData to data class
daniele-verducci Nov 3, 2026
41b6916
Moved activity model into adapter pkg
daniele-verducci Nov 3, 2026
aa84855
Moved ActivityViewHolder to its own file
daniele-verducci Nov 3, 2026
0547caa
Moved ActivityListItem into model
daniele-verducci Nov 3, 2026
ce3510b
Moved VersionsResult into model
daniele-verducci Nov 3, 2026
e0eebdf
Moved ActivitiesResult into model
daniele-verducci Nov 3, 2026
9698d70
Moved RestoreVersionsResult into model
daniele-verducci Nov 3, 2026
64c7b26
More kotlin-idiomatic function
daniele-verducci Nov 3, 2026
25a1d14
Missing license
daniele-verducci Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions LICENSES/BSD-2-Clause.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Copyright (c) <year> <owner>

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 changes: 5 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* SPDX-FileCopyrightText: 2020 Hui-Ouyang16 <11710106@mail.sustech.edu.cn>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@file:Suppress("MagicNumber")

plugins {
id("com.android.application")
id("com.google.devtools.ksp")
}

val kotlinVersion = rootProject.extra["kotlinVersion"] as String
Expand Down Expand Up @@ -130,6 +132,7 @@ dependencies {
implementation("commons-httpclient:commons-httpclient:3.1") {
exclude(group = "commons-logging", module = "commons-logging")
}
implementation("org.apache.jackrabbit:jackrabbit-webdav:2.13.5")

implementation("com.github.nextcloud:android-library:2.26.1") {
exclude(group = "org.ogce", module = "xpp3")
Expand All @@ -156,7 +159,8 @@ dependencies {
// Glide
val glideVersion = "5.0.9"
implementation("com.github.bumptech.glide:glide:$glideVersion")
annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion")
ksp("com.github.bumptech.glide:ksp:$glideVersion")
implementation("com.caverock:androidsvg-aar:1.4")

// Android X
implementation("androidx.appcompat:appcompat:1.8.0")
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</activity>

<activity
android:name=".share.NoteShareActivity" />
android:name=".itemdetails.ItemDetailsActivity" />

<activity
android:name=".share.NoteShareDetailActivity" />
Expand Down
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.model.ActivitiesResult
import it.niedermann.owncloud.notes.activities.repository.VersionsRepository
import it.niedermann.owncloud.notes.activities.repository.model.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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* 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.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.adapter.model.Activity
import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository
import it.niedermann.owncloud.notes.activities.repository.model.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

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) = Unit

override fun onVersionItemClick(version: FileVersion) {
showProgress(true)
lifecycleScope.launch(Dispatchers.IO) {
// Restore the version
val ssoAccount =
SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext())
val repository = VersionsRepository(requireContext(), ssoAccount)
val result = repository.restoreVersion(version)
withContext(Dispatchers.Main) {
when (result) {
RestoreVersionsResult.Success -> syncNote()
RestoreVersionsResult.FailedUnsupported ->
showMessageAndHideProgress(R.string.version_restore_failed_unsupported)
else -> showMessageAndHideProgress(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
showMessageAndHideProgress(R.string.version_restored)
})
noteRepo.scheduleSync(account, false)
}

private fun showProgress(show: Boolean) { binding.swipeContainingList.isRefreshing = show }

private fun showMessageAndHideProgress(@StringRes message: Int) {
showProgress(false)
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() {
showProgress(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() {
showProgress(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 = ActivitiesFragment().apply {
arguments = Bundle().apply {
putSerializable(ARG_NOTE, note)
putSerializable(ARG_ACCOUNT, account)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.adapter.model.Activity
import it.niedermann.owncloud.notes.activities.adapter.model.ActivityListItem
import it.niedermann.owncloud.notes.activities.adapter.model.ActivityListItem.FileVersionItem

data class ActivitiesData(
val activities: ArrayList<Activity> = arrayListOf(),
val versions: ArrayList<FileVersion> = arrayListOf(),
var activitiesLastGiven: Long? = null,
var activitiesMore: Boolean = true
) {
fun getAggregatedList(context: Context) : List<ActivityListItem> {
val items = arrayListOf<ActivityListItem>()
items.addAll(
activities.map { ActivityListItem.ActivityItem(it) }
)
items.addAll(
versions.map { FileVersionItem(context, it) }
)
items.sortDescending()
return items
}

fun isEmpty() : Boolean = activities.isEmpty() && versions.isEmpty()
}
Loading
Loading