Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b00b6b2
Added menu item
daniele-verducci Oct 6, 2026
4720d0c
Item detail layout and resources
daniele-verducci Oct 6, 2026
9dd2668
Stubbed activities fragment, implemented ItemDetailsActivity with pag…
daniele-verducci Oct 6, 2026
2a1789f
Replaced menu items
daniele-verducci Oct 6, 2026
409e130
Manifest
daniele-verducci Oct 6, 2026
7c9022b
wip
daniele-verducci Oct 6, 2026
ecedef2
Converted NoteShareActivity.java to Kotlin (automated conversion, no …
daniele-verducci Oct 12, 2026
7ad562b
Refactored NoteShareActivity to be more kotlin idiomatic
daniele-verducci Oct 12, 2026
78a3b9d
Converted NoteShareActivity to fragment
daniele-verducci Oct 12, 2026
8e6cb65
Removed stale code
daniele-verducci Oct 12, 2026
b7a5d57
Cleanup: the searchable <meta-data> wasn't present in the activity de…
daniele-verducci Oct 12, 2026
83b2a52
WIP passing required parameters
daniele-verducci Oct 12, 2026
4a7bad1
Wired new details screen in place of old share, preserved system shar…
daniele-verducci Oct 13, 2026
f1f9590
Moved collectNoteContents to worker thread to fix lint error (already…
daniele-verducci Oct 13, 2026
fb0cc62
Fixed view name
daniele-verducci Oct 13, 2026
c232c74
Header content, tabs icons, theming
daniele-verducci Oct 13, 2026
5aeb155
Actionbar
daniele-verducci Oct 13, 2026
0f8f92d
Removed size (we don't have such info right now)
daniele-verducci Oct 13, 2026
2fdb54e
Stubbed activities API and repository
daniele-verducci Oct 13, 2026
2be15a7
Missing copyright headers
daniele-verducci Oct 14, 2026
96c9bfb
Activities list layout
daniele-verducci Oct 14, 2026
9f0a086
Implemented activities APIs
daniele-verducci Oct 14, 2026
9968ae8
Implemented pagination for activities API
daniele-verducci Oct 14, 2026
c041832
Implemented activities list, fix activity optional fields
daniele-verducci Oct 14, 2026
4141dcb
Fix
daniele-verducci Oct 14, 2026
eb79668
Empty list
daniele-verducci Oct 14, 2026
d0a1a10
Pull-to-refresh
daniele-verducci Oct 14, 2026
9f739d0
List incremental loading
daniele-verducci Oct 14, 2026
0448a85
WIP icons in activities list
daniele-verducci Oct 14, 2026
cc00f90
Prevent Glide to rewrite the url and point to the WebDAV storage for …
daniele-verducci Oct 15, 2026
2c6707d
Converted CustomAppGlideModule to kotlin
daniele-verducci Oct 15, 2026
e329779
More kotlin-idiomatic CustomAppGlideModule
daniele-verducci Oct 15, 2026
6ab6d17
Added SVG transcoder to CustomAppGlideModule (note: added dependency …
daniele-verducci Oct 15, 2026
d8a5ebb
Added missing SvgDecoder (from Files app)
daniele-verducci Oct 15, 2026
a4775b2
Fixed Glide module not loaded since it was converted to Kotlin few co…
daniele-verducci Oct 15, 2026
fa63072
gradlew --write-verification-metadata
daniele-verducci Oct 15, 2026
657b0e4
Implemented Versions API (note: added dependency jackrabbit-webdav, a…
daniele-verducci Oct 16, 2026
547ee20
Implemented Versions repo
daniele-verducci Oct 16, 2026
6bcf17b
Fixed Versions call failing because unauthenticated
daniele-verducci Oct 16, 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

apply plugin: 'com.android.application'
apply plugin: 'com.google.devtools.ksp'

android {
namespace = 'it.niedermann.owncloud.notes'
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 @@ -155,7 +157,8 @@ dependencies {

// Glide
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,185 @@
package it.niedermann.owncloud.notes.activities

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.nextcloud.android.sso.helper.SingleAccountHelper
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.ActivitiesResult
import it.niedermann.owncloud.notes.activities.repository.VersionsRepository
import it.niedermann.owncloud.notes.activities.repository.VersionsResult
import it.niedermann.owncloud.notes.branding.BrandedFragment
import it.niedermann.owncloud.notes.branding.BrandingUtil
import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding
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
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService

/*
* 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 {

lateinit var binding : FragmentActivitiesBinding
private lateinit var note: Note
private lateinit var account: Account
private lateinit var adapter: ActivityListAdapter
private var lastGiven: Long? = null

private var executorService: ScheduledExecutorService? = null


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
executorService = Executors.newSingleThreadScheduledExecutor()
binding = FragmentActivitiesBinding.inflate(inflater, container, false)

initializeArguments()
initializeList()
fetchActivities()

//DEBUG
fetchVersions()
//DEBUG

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() {
adapter = ActivityListAdapter(
requireContext(),
this
)
binding.list.adapter = adapter
binding.swipeContainingList.setOnRefreshListener { fetchActivities() }
}

override fun applyBrand(color: Int) {
val util = BrandingUtil.of(color, requireContext())
util.platform.colorViewBackground(binding.getRoot())
util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList)
}

private fun fetchActivities(reqLastGiven: Long? = null) {
val noteId = note.remoteId ?: return

binding.swipeContainingList.isRefreshing = true

executorService!!.submit(Runnable submit@{
val ssoAccount =
SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext())
val repository = ActivitiesRepository(requireContext(), ssoAccount)
lifecycleScope.launch(Dispatchers.IO) {
val result = repository.fetchActivities(noteId, reqLastGiven)
withContext(Dispatchers.Main) {
when (result) {
is ActivitiesResult.Success -> {
val isFirstPage = reqLastGiven == null || lastGiven == null
lastGiven = result.lastGiven
showActivities(result.activities, isFirstPage)
}

is ActivitiesResult.FailedNotModified -> {
Log.i(TAG, "Reached end of list (last page starting from item $lastGiven")
}

is ActivitiesResult.Failed -> {
adapter.clear()
showEmptyContent(getString(R.string.activities_error))
}
}

binding.swipeContainingList.isRefreshing = false
}
}

})
}

private fun fetchVersions() {
val noteId = note.remoteId ?: return
executorService!!.submit(Runnable submit@{
val ssoAccount =
SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext())
val repository = VersionsRepository(requireContext(), ssoAccount)
lifecycleScope.launch(Dispatchers.IO) {
val result = repository.fetchVersions(noteId)
withContext(Dispatchers.Main) {
when (result) {
is VersionsResult.Success -> {
result.versions.forEach {
Log.d("DEBUG", "Version: ${it.remoteId} ${it.fileName} ${it.modifiedTimestamp}")
}
}

is VersionsResult.Failed -> {
Log.e("DEBUG", "Versions failed")
}
}
}
}
})
}

private fun showActivities(activities: List<Activity>, isFirstPage: Boolean) {
binding.emptyList.visibility = View.GONE
if (isFirstPage) adapter.clear()
adapter.addItems(activities)
}

private fun showEmptyContent(message: String) =
with (binding.emptyList) {
visibility = View.VISIBLE
text = message
}

override fun onItemClick(activity: Activity) {
TODO("Not yet implemented")
}

override fun onEndOfListReached() {
if (adapter.itemCount < ActivitiesRepository.PAGE_SIZE)
return
fetchActivities(lastGiven)
}

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,121 @@
package it.niedermann.owncloud.notes.activities.adapter

import android.annotation.SuppressLint
import android.content.Context
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import it.niedermann.owncloud.notes.R
import it.niedermann.owncloud.notes.activities.model.Activity
import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding

class ActivityListAdapter (
private val context: Context,
private val listener: OnActivityClickListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private val items = arrayListOf<Activity>()

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder =
ActivityViewHolder(
ActivityListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (position == itemCount - 1) {
listener.onEndOfListReached()
}
(holder as ActivityViewHolder).bind(items[position], listener)
}

override fun getItemCount(): Int = items.size

fun addItems(it: List<Activity>) {
val prevItems = itemCount
this.items.addAll(it)
notifyItemRangeInserted(prevItems, it.size)
}

@SuppressLint("NotifyDataSetChanged")
fun clear() {
this.items.clear()
notifyDataSetChanged()
}

private class ActivityViewHolder(
val binding: ActivityListItemBinding,
//val brandingUtil: BrandingUtil
) : RecyclerView.ViewHolder(binding.root) {

fun bind(activity: Activity, listener: OnActivityClickListener) {
bindDateTime(activity)
bindSubject(activity)
bindMessage(activity)
bindIcon(activity)
binding.root.setOnClickListener { listener.onItemClick(activity) }
}

private fun bindDateTime(activity: Activity) {
binding.datetime.apply {
visibility = View.VISIBLE
text = DateFormat.format(TIME_PATTERN, activity.datetime.time)
}
}

private fun bindSubject(activity: Activity) {
binding.subject.text = activity.subject
}

private fun bindMessage(activity: Activity) {
binding.message.apply {
when {
activity.message.isNullOrEmpty() -> visibility = View.GONE
else -> {
visibility = View.VISIBLE
text = activity.message
}
}
}
}

private fun bindIcon(activity: Activity) {
if (activity.icon.isNullOrEmpty()) {
return
}

Glide
.with(binding.icon.context)
.load(activity.icon.toUri())
.placeholder( R.drawable.ic_activity)
.error(R.drawable.ic_activity)
.into(binding.icon)

/*
if (activity.icon.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) {
binding.icon.imageTintList = null
} else {
brandingUtil.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT)
}
*/

}

companion object {
private const val TIME_PATTERN = "HH:mm"
private const val COLORED_ICON_SUFFIX = "-color.svg"
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package it.niedermann.owncloud.notes.activities.adapter

import it.niedermann.owncloud.notes.activities.model.Activity

interface OnActivityClickListener {
fun onItemClick(activity: Activity)
fun onEndOfListReached()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package it.niedermann.owncloud.notes.activities.model

import com.google.gson.annotations.SerializedName
import java.util.Date

data class Activity(
@SerializedName("activity_id") val id: Long,
val app: String,
val type: String,
val user: String,
val subject: String,
val message: String?,
val icon: String?,
val link: String?,
val datetime: Date,
@SerializedName("object_id") val objectId: Long?,
@SerializedName("object_name") val objectName: String?
)
Loading