Skip to content
Open
Show file tree
Hide file tree
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 Oct 6, 2026
5c93ff7
Item detail layout and resources
daniele-verducci Oct 6, 2026
e79e0b7
Stubbed activities fragment, implemented ItemDetailsActivity with pag…
daniele-verducci Oct 6, 2026
20a7b2a
Replaced menu items
daniele-verducci Oct 6, 2026
8ef2e7b
Manifest
daniele-verducci Oct 6, 2026
13df6bb
wip
daniele-verducci Oct 6, 2026
83c9578
Converted NoteShareActivity.java to Kotlin (automated conversion, no …
daniele-verducci Oct 12, 2026
aaa0afb
Refactored NoteShareActivity to be more kotlin idiomatic
daniele-verducci Oct 12, 2026
7bb54db
Converted NoteShareActivity to fragment
daniele-verducci Oct 12, 2026
0c739bd
Removed stale code
daniele-verducci Oct 12, 2026
5eae9ba
Cleanup: the searchable <meta-data> wasn't present in the activity de…
daniele-verducci Oct 12, 2026
aaf55bb
WIP passing required parameters
daniele-verducci Oct 12, 2026
c3bf76e
Wired new details screen in place of old share, preserved system shar…
daniele-verducci Oct 13, 2026
283510b
Moved collectNoteContents to worker thread to fix lint error (already…
daniele-verducci Oct 13, 2026
e8b755a
Fixed view name
daniele-verducci Oct 13, 2026
bf0c1fa
Header content, tabs icons, theming
daniele-verducci Oct 13, 2026
03e5773
Actionbar
daniele-verducci Oct 13, 2026
55cdf7f
Removed size (we don't have such info right now)
daniele-verducci Oct 13, 2026
f53d63f
Stubbed activities API and repository
daniele-verducci Oct 13, 2026
2a6f7b3
Missing copyright headers
daniele-verducci Oct 14, 2026
003937e
Activities list layout
daniele-verducci Oct 14, 2026
fe13e5c
Implemented activities APIs
daniele-verducci Oct 14, 2026
e938488
Implemented pagination for activities API
daniele-verducci Oct 14, 2026
2c47298
Implemented activities list, fix activity optional fields
daniele-verducci Oct 14, 2026
12a0f00
Fix
daniele-verducci Oct 14, 2026
a83711d
Empty list
daniele-verducci Oct 14, 2026
b2ff5fa
Pull-to-refresh
daniele-verducci Oct 14, 2026
92b3d26
List incremental loading
daniele-verducci Oct 14, 2026
b3062f1
WIP icons in activities list
daniele-verducci Oct 14, 2026
3a32b76
Prevent Glide to rewrite the url and point to the WebDAV storage for …
daniele-verducci Oct 15, 2026
faa55ae
Converted CustomAppGlideModule to kotlin
daniele-verducci Oct 15, 2026
dd7da4b
More kotlin-idiomatic CustomAppGlideModule
daniele-verducci Oct 15, 2026
2f1a1f9
Added SVG transcoder to CustomAppGlideModule (note: added dependency …
daniele-verducci Oct 15, 2026
46ed282
Added missing SvgDecoder (from Files app)
daniele-verducci Oct 15, 2026
3844c61
Fixed Glide module not loaded since it was converted to Kotlin few co…
daniele-verducci Oct 15, 2026
645d14f
gradlew --write-verification-metadata
daniele-verducci Oct 15, 2026
c437b94
Implemented Versions API (note: added dependency jackrabbit-webdav, a…
daniele-verducci Oct 16, 2026
9e5103e
Implemented Versions repo
daniele-verducci Oct 16, 2026
6b9d13d
Fixed Versions call failing because unauthenticated
daniele-verducci Oct 16, 2026
c338097
Implemented Activity/Versions ordered merging and updated adapter to …
daniele-verducci Oct 19, 2026
d8cdf04
Fixed activity pagination (fileVersion endpoint donesn't have paginat…
daniele-verducci Oct 19, 2026
261e9f2
Applied branding color
daniele-verducci Oct 19, 2026
cdcaebe
List loading/refresh
daniele-verducci Oct 19, 2026
741424b
Restore version action icon
daniele-verducci Oct 19, 2026
5aab38d
Implemented version restore, but MOVE Dav method is blocked by SSO li…
daniele-verducci Oct 19, 2026
49ccd7f
Updated MOVE WebDAV request call to SSO lib, user-readable error in c…
daniele-verducci Oct 20, 2026
a395ce1
Refactored entire data fetch and presentation
daniele-verducci Oct 20, 2026
0d5b22a
Fixes
daniele-verducci Oct 20, 2026
f11a7be
Same order as web version, fixed pagination
daniele-verducci Oct 21, 2026
03ee20f
Unnecessary lateinit
daniele-verducci Oct 21, 2026
7b66b98
Edge case fix + restored missing error message
daniele-verducci Oct 22, 2026
394e966
Prevent empty calls to activities
daniele-verducci Oct 22, 2026
6ca16f1
Fixed empty list on servers with no Activities app
daniele-verducci Oct 22, 2026
ff3d4d6
Dates in list
daniele-verducci Oct 22, 2026
7273035
Cleaner repo impl, fixed load loop
daniele-verducci Oct 22, 2026
d759097
Catch exception
daniele-verducci Oct 22, 2026
4d5b7a1
Fix wrong page loaded
daniele-verducci Oct 22, 2026
f563a00
Close req
daniele-verducci Oct 22, 2026
dc1f64c
3-dots button in list alighed to Files app
daniele-verducci Oct 23, 2026
aeaa83e
Added empty bottom sheet
daniele-verducci Oct 23, 2026
fd061e1
Init bottom sheet layout
daniele-verducci Oct 23, 2026
c77de5e
Moved file to its own package
daniele-verducci Oct 23, 2026
a594c77
Completed bottom sheet implementation
daniele-verducci Oct 23, 2026
ebc1284
Branding, aligned info screen to Files'
daniele-verducci Oct 23, 2026
b1f4598
Fix crash
daniele-verducci Oct 23, 2026
f495f9d
Safer context
daniele-verducci Oct 23, 2026
0623706
Added missing headers
daniele-verducci Oct 23, 2026
5ce2cc8
Format file sizes
daniele-verducci Oct 23, 2026
6608667
Fix
daniele-verducci Oct 23, 2026
d46d7bf
Hide info button when more than one element is selected
daniele-verducci Oct 23, 2026
17c02b5
Sync note after version restore and notify editor show updated note w…
daniele-verducci Oct 23, 2026
abc4692
Updated SSO library to a version supporting MOVE WebDAV method
daniele-verducci Oct 28, 2026
e709189
Fix verification-metadata
daniele-verducci Oct 28, 2026
64763fe
Avoid creating a Calendar instance for every item
daniele-verducci Oct 28, 2026
eb76c7a
Cast
daniele-verducci Oct 28, 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
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

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

val kotlinVersion = rootProject.extra["kotlinVersion"] as String
Expand Down Expand Up @@ -130,6 +131,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.0") {
exclude(group = "org.ogce", module = "xpp3")
Expand All @@ -156,7 +158,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.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()
}
}
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 =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val ssoAccount =
            SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext())

Can this be done on main thread?

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
}
}
}
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()
}
Loading
Loading