From 4c6b6c0d3349c9d7c737fd09ea83404ed8b56d2c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:11:41 +0200 Subject: [PATCH 01/96] Added menu item Signed-off-by: daniele-verducci --- .../main/MultiSelectedActionModeCallback.java | 2 ++ app/src/main/res/drawable/ic_activity.xml | 16 ++++++++++++++++ .../main/res/menu/menu_list_context_multiple.xml | 7 +++++++ app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 27 insertions(+) create mode 100644 app/src/main/res/drawable/ic_activity.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index a80ac4325..01f169c2b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -186,6 +186,8 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { .show(fragmentManager, CategoryDialogFragment.class.getSimpleName()); }); return true; + } else if (itemId == R.id.menu_activities) { + } return false; } diff --git a/app/src/main/res/drawable/ic_activity.xml b/app/src/main/res/drawable/ic_activity.xml new file mode 100644 index 000000000..a9505658c --- /dev/null +++ b/app/src/main/res/drawable/ic_activity.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/menu/menu_list_context_multiple.xml b/app/src/main/res/menu/menu_list_context_multiple.xml index 1a8491212..8ea26af35 100644 --- a/app/src/main/res/menu/menu_list_context_multiple.xml +++ b/app/src/main/res/menu/menu_list_context_multiple.xml @@ -22,6 +22,13 @@ android:title="@string/menu_share" app:showAsAction="ifRoom" /> + + Share expires on %1$s Dismiss Account not found! + + Activities From 7088e3ffa3ab30f75db7fabc83d261dfedf526ed Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:12:16 +0200 Subject: [PATCH 02/96] Item detail layout and resources Signed-off-by: daniele-verducci --- .../main/res/layout/activity_item_details.xml | 117 ++++++++++++++++++ app/src/main/res/values-night/colors.xml | 3 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 6 + 5 files changed, 133 insertions(+) create mode 100644 app/src/main/res/layout/activity_item_details.xml diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml new file mode 100644 index 000000000..af3c40443 --- /dev/null +++ b/app/src/main/res/layout/activity_item_details.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 0857691c8..4e43ed338 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -45,4 +45,7 @@ @color/material_dynamic_secondary20 @color/transparent + + #49454F + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f3a39be8a..c5649baf1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -59,4 +59,9 @@ @color/high_emphasis_text @color/transparent + + #CAC4D0 + @color/secondary_text_color + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b42e9c41b..d842dc8ae 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -82,4 +82,6 @@ 56dp 60dp 24dp + + @dimen/spacer_2x diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e105d3bb2..93b3c86af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -548,4 +548,10 @@ Account not found! Activities + Note.md + Favorite + 389 KB +  • + 12:23:45 + From 6e4e838db955f4c28f55ca56d3aa329e1f054454 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:22:17 +0200 Subject: [PATCH 03/96] Stubbed activities fragment, implemented ItemDetailsActivity with pager and fragments Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 28 ++++++++++++ .../notes/itemdetails/ItemDetailsActivity.kt | 44 +++++++++++++++++++ .../main/res/layout/fragment_activities.xml | 11 +++++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 85 insertions(+) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt create mode 100644 app/src/main/res/layout/fragment_activities.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt new file mode 100644 index 000000000..f95cb1448 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -0,0 +1,28 @@ +package it.niedermann.owncloud.notes.activities + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding + +class ActivitiesFragment : BrandedFragment() { + + lateinit var binding : FragmentActivitiesBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentActivitiesBinding.inflate(inflater) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun applyBrand(color: Int) { + // TODO + } + + companion object { + fun newInstance() : ActivitiesFragment { + return ActivitiesFragment() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt new file mode 100644 index 000000000..59a8c3efe --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -0,0 +1,44 @@ +package it.niedermann.owncloud.notes.itemdetails + +import android.os.Bundle +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayoutMediator +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.activities.ActivitiesFragment +import it.niedermann.owncloud.notes.branding.BrandedActivity +import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.databinding.ActivityItemDetailsBinding +import it.niedermann.owncloud.notes.share.NoteShareActivity + +class ItemDetailsActivity : BrandedActivity() { + + lateinit var binding : ActivityItemDetailsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityItemDetailsBinding.inflate(layoutInflater) + setContentView(binding.root) + + val pages = listOf>( + Pair(getString(R.string.activities_title), ActivitiesFragment.newInstance()) + //Pair(getString(R.string.share), NoteShareFragment.newInstance()) + ) + + binding.pager.adapter = PagerAdapter(pages) + TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> + tab.text = pages[position].first + }.attach() + } + + override fun applyBrand(color: Int) { + // TODO + } + + private inner class PagerAdapter(val fragments: List>) : + FragmentStateAdapter(this@ItemDetailsActivity) + { + override fun getItemCount() = fragments.size + override fun createFragment(position: Int) = fragments[position].second + } + +} diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml new file mode 100644 index 000000000..3578d96f5 --- /dev/null +++ b/app/src/main/res/layout/fragment_activities.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93b3c86af..e7f5035d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -548,6 +548,8 @@ Account not found! Activities + Activities + Note.md Favorite 389 KB From 3270390a9320f54a1218c5d063d030c7be882af0 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:34:08 +0200 Subject: [PATCH 04/96] Replaced menu items Signed-off-by: daniele-verducci --- .../owncloud/notes/edit/BaseNoteFragment.java | 7 +++++-- .../owncloud/notes/edit/NoteReadonlyFragment.java | 2 +- .../notes/main/MultiSelectedActionModeCallback.java | 10 ++++++---- .../main/res/menu/menu_list_context_multiple.xml | 13 +++---------- app/src/main/res/menu/menu_note_fragment.xml | 6 +++--- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index f64eec2e7..d41d13675 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -46,6 +46,7 @@ import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener; import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment; import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; @@ -305,8 +306,10 @@ public boolean onOptionsItemSelected(MenuItem item) { .newInstance(new ArrayList<>(repo.getAccounts()), note.getAccountId()) .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; - } else if (itemId == R.id.menu_share) { - shareNote(); + } else if (itemId == R.id.menu_details) { + Intent detailsIntent = new Intent(requireContext(), ItemDetailsActivity.class); + startActivity(detailsIntent); + //shareNote(); // TODO return false; } else if (itemId == MENU_ID_PIN) { pinNoteToHome(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java index 7a1126f59..9e17cc55e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java @@ -28,7 +28,7 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) { menu.findItem(R.id.menu_preview).setVisible(false); menu.findItem(R.id.menu_cancel).setVisible(false); menu.findItem(R.id.menu_delete).setVisible(false); - menu.findItem(R.id.menu_share).setVisible(false); + menu.findItem(R.id.menu_details).setVisible(false); menu.findItem(R.id.menu_move).setVisible(false); menu.findItem(R.id.menu_category).setVisible(false); menu.findItem(R.id.menu_title).setVisible(false); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 01f169c2b..463c66c7a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -33,6 +33,7 @@ import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.share.NoteShareActivity; import it.niedermann.owncloud.notes.shared.util.ShareUtil; @@ -153,7 +154,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName())); }); return true; - } else if (itemId == R.id.menu_share) { + } /*else if (itemId == R.id.menu_share) { final var selection = new ArrayList(tracker.getSelection().size()); for (final var sel : tracker.getSelection()) { selection.add(sel); @@ -177,7 +178,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { } return true; - } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it + }*/ else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it final var accountLiveData = mainViewModel.getCurrentAccount(); accountLiveData.observe(lifecycleOwner, account -> { accountLiveData.removeObservers(lifecycleOwner); @@ -186,8 +187,9 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { .show(fragmentManager, CategoryDialogFragment.class.getSimpleName()); }); return true; - } else if (itemId == R.id.menu_activities) { - + } else if (itemId == R.id.menu_details) { + Intent detailsIntent = new Intent(context, ItemDetailsActivity.class); + mainActivity.startActivity(detailsIntent); } return false; } diff --git a/app/src/main/res/menu/menu_list_context_multiple.xml b/app/src/main/res/menu/menu_list_context_multiple.xml index 8ea26af35..66f5c34b8 100644 --- a/app/src/main/res/menu/menu_list_context_multiple.xml +++ b/app/src/main/res/menu/menu_list_context_multiple.xml @@ -16,17 +16,10 @@ app:showAsAction="ifRoom" /> - - Dismiss Account not found! - Activities + Details Activities Note.md From 47d997d3a20b099088ded64b9e24cb372c626a08 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:36:25 +0200 Subject: [PATCH 05/96] Manifest Signed-off-by: daniele-verducci --- app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 111a0dc01..2f8eb23ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,7 +58,7 @@ + android:name=".itemdetails.ItemDetailsActivity" /> From 87f592c9a6b17185d4cc4e76d619476fd5f885a9 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:44:50 +0200 Subject: [PATCH 06/96] wip Signed-off-by: daniele-verducci --- .../niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt | 1 + app/src/main/res/values/strings.xml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 59a8c3efe..c310775c8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -21,6 +21,7 @@ class ItemDetailsActivity : BrandedActivity() { val pages = listOf>( Pair(getString(R.string.activities_title), ActivitiesFragment.newInstance()) + // TODO: convert NoteShareActivity to Kotlin and refactor to be a BrandedFragment //Pair(getString(R.string.share), NoteShareFragment.newInstance()) ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08dccae22..83ecfeaf1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,6 @@ Category Favorite Preview - Share Shared note Loading… From fe659a3e4e7347e15cc88229def78ca9d704ef4c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 17:16:51 +0200 Subject: [PATCH 07/96] Converted NoteShareActivity.java to Kotlin (automated conversion, no refactoring) Signed-off-by: daniele-verducci --- .../notes/share/NoteShareActivity.java | 903 ----------------- .../owncloud/notes/share/NoteShareActivity.kt | 945 ++++++++++++++++++ 2 files changed, 945 insertions(+), 903 deletions(-) delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java deleted file mode 100644 index a69cc8c0c..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java +++ /dev/null @@ -1,903 +0,0 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.niedermann.owncloud.notes.share; - -import android.Manifest; -import android.app.Activity; -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.text.InputType; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; - -import androidx.activity.OnBackPressedCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.widget.SearchView; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.android.sso.helper.SingleAccountHelper; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.lib.resources.shares.ShareType; - -import org.jetbrains.annotations.NotNull; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedActivity; -import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding; -import it.niedermann.owncloud.notes.main.MainActivity; -import it.niedermann.owncloud.notes.persistence.ApiResult; -import it.niedermann.owncloud.notes.persistence.ApiResultKt; -import it.niedermann.owncloud.notes.persistence.entity.Account; -import it.niedermann.owncloud.notes.persistence.entity.Note; -import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter; -import it.niedermann.owncloud.notes.share.adapter.SuggestionAdapter; -import it.niedermann.owncloud.notes.share.dialog.NoteShareActivityShareItemActionBottomSheetDialog; -import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog; -import it.niedermann.owncloud.notes.share.dialog.ShareLinkToDialog; -import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment; -import it.niedermann.owncloud.notes.share.helper.AvatarLoader; -import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider; -import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction; -import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; -import it.niedermann.owncloud.notes.share.model.CreateShareResponse; -import it.niedermann.owncloud.notes.share.model.CreateShareResponseExtensionsKt; -import it.niedermann.owncloud.notes.share.model.UpdateShareRequest; -import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; -import it.niedermann.owncloud.notes.share.repository.ShareRepository; -import it.niedermann.owncloud.notes.shared.model.Capabilities; -import it.niedermann.owncloud.notes.shared.model.OcsResponse; -import it.niedermann.owncloud.notes.shared.util.DisplayUtils; -import it.niedermann.owncloud.notes.shared.util.ShareUtil; -import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil; -import it.niedermann.owncloud.notes.shared.util.extensions.BundleExtensionsKt; -import it.niedermann.owncloud.notes.util.DateUtil; - -public class NoteShareActivity extends BrandedActivity implements ShareeListAdapterListener, NoteShareItemAction, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions, SharePasswordDialogFragment.SharePasswordDialogListener { - - private static final String TAG = "NoteShareActivity"; - public static final String ARG_NOTE = "NOTE"; - public static final String ARG_ACCOUNT = "ACCOUNT"; - public static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG"; - - private ScheduledExecutorService executorService; - private Future future; - private static final long SEARCH_DELAY_MS = 500; - - private ActivityNoteShareBinding binding; - private Note note; - private Account account; - private ShareRepository repository; - private Capabilities capabilities; - private ActivityResultLauncher resultLauncher; - private final List shares = Collections.synchronizedList(new ArrayList<>()); - - public static Intent getActivityStartIntent(@NonNull Note note, @NonNull Account account, @NonNull Activity activity) { - Bundle bundle = new Bundle(); - bundle.putSerializable(NoteShareActivity.ARG_NOTE, note); - bundle.putSerializable(NoteShareActivity.ARG_ACCOUNT, account); - - Intent intent = new Intent(activity, NoteShareActivity.class); - intent.putExtras(bundle); - return intent; - } - - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - executorService = Executors.newSingleThreadScheduledExecutor(); - binding = ActivityNoteShareBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - binding.toolbar.setNavigationOnClickListener(v -> backPressed()); - registerResultLauncher(); - initializeArguments(); - initializeOnBackPressedDispatcher(); - } - - private void registerResultLauncher() { - resultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null) { - recreate(); - } - }); - } - - private void initializeOnBackPressedDispatcher() { - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - backPressed(); - } - }); - } - - private void backPressed() { - Intent intent = new Intent(NoteShareActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivity(intent); - finish(); - } - - private void initializeArguments() { - Bundle bundler = getIntent().getExtras(); - note = BundleExtensionsKt.getSerializableArgument(bundler, ARG_NOTE, Note.class); - account = BundleExtensionsKt.getSerializableArgument(bundler, ARG_ACCOUNT, Account.class); - - if (note == null) { - throw new IllegalArgumentException("Note cannot be null"); - } - - if (account == null) { - throw new IllegalArgumentException("Account cannot be null"); - } - - executorService.submit(() -> { - try { - final var ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(NoteShareActivity.this); - repository = new ShareRepository(NoteShareActivity.this, ssoAcc); - capabilities = repository.getCapabilities(); - repository.fetchSharesForNotesAndSaveShareEntities(); - - runOnUiThread(() -> { - binding.fileName.setText(note.getTitle()); - binding.searchContainer.setVisibility(View.VISIBLE); - binding.sharesList.setVisibility(View.VISIBLE); - binding.sharesList.setAdapter(new ShareeListAdapter(this, new ArrayList<>(), this, account)); - binding.sharesList.setLayoutManager(new LinearLayoutManager(this)); - binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); - binding.btnShareButton.setOnClickListener(v -> ShareUtil.openShareDialog(this, note.getTitle(), note.getContent())); - - if (note.getReadonly()) { - setupReadOnlySearchView(); - } else { - setupSearchView((SearchManager) getSystemService(Context.SEARCH_SERVICE), getComponentName()); - } - - updateShareeListAdapter(); - binding.loadingLayout.setVisibility(View.GONE); - }); - } catch (Exception e) { - Log_OC.e(TAG, "Exception at NoteShareActivity.init: " + e); - } - }); - } - - @Override - public void onStart() { - super.onStart(); - - // note cannot be encrypted - logic ported from files app - UsersAndGroupsSearchConfig.INSTANCE.setSearchOnlyUsers(false); - } - - @Override - public void onStop() { - super.onStop(); - UsersAndGroupsSearchConfig.INSTANCE.reset(); - } - - private void disableSearchView(View view) { - view.setEnabled(false); - - if (view instanceof ViewGroup viewGroup) { - for (int i = 0; i < viewGroup.getChildCount(); i++) { - disableSearchView(viewGroup.getChildAt(i)); - } - } - } - - private void setupReadOnlySearchView() { - binding.searchView.setIconifiedByDefault(false); - binding.searchView.setQueryHint(getResources().getString(R.string.note_share_activity_resharing_not_allowed)); - binding.searchView.setInputType(InputType.TYPE_NULL); - binding.pickContactEmailBtn.setVisibility(View.GONE); - disableSearchView(binding.searchView); - } - - private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { - if (searchManager == null) { - binding.searchView.setVisibility(View.GONE); - return; - } - - SuggestionAdapter suggestionAdapter = new SuggestionAdapter(this, null, account); - UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(this, repository); - - binding.searchView.setSuggestionsAdapter(suggestionAdapter); - binding.searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)); - binding.searchView.setIconifiedByDefault(false); - binding.searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); - binding.searchView.setQueryHint(getResources().getString(R.string.note_share_activity_search_text)); - binding.searchView.setInputType(InputType.TYPE_CLASS_TEXT); - binding.searchView.setFocusable(true); - binding.searchView.setFocusableInTouchMode(true); - - View closeButton = binding.searchView.findViewById(androidx.appcompat.R.id.search_close_btn); - closeButton.setOnClickListener(v -> { - binding.progressBar.setVisibility(View.GONE); - binding.searchView.setQuery("", false); - }); - - binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - // return true to prevent the query from being processed; - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - if (!newText.isEmpty()) { - binding.progressBar.setVisibility(View.VISIBLE); - - // Cancel the previous task if it's still running - if (future != null && !future.isDone()) { - future.cancel(true); - } - - // Schedule a new task with a delay - future = executorService.schedule(() -> { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return; - } - - final var isFederationShareAllowed = capabilities.getFederationShare(); - try { - var cursor = provider.searchForUsersOrGroups(newText, isFederationShareAllowed); - - if (cursor == null || cursor.getCount() == 0) { - return; - } - - runOnUiThread(() -> { - if (binding.searchView.getVisibility() == View.VISIBLE) { - suggestionAdapter.swapCursor(cursor); - } - - binding.progressBar.setVisibility(View.GONE); - }); - - } catch (Exception e) { - Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: " + e); - runOnUiThread(() -> binding.progressBar.setVisibility(View.GONE)); - } - - }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS); - } else { - binding.progressBar.setVisibility(View.GONE); - suggestionAdapter.swapCursor(null); - } - return false; - } - }); - - binding.searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(binding.searchView.findFocus(), InputMethodManager.SHOW_IMPLICIT); - } - }); - - binding.searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { - @Override - public boolean onSuggestionSelect(int position) { - return false; - } - - @Override - public boolean onSuggestionClick(int position) { - Cursor cursor = suggestionAdapter.getCursor(); - if (cursor != null && cursor.moveToPosition(position)) { - String suggestion = cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)); - binding.searchView.setQuery(suggestion, false); - - String shareWith = cursor.getString(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_WITH)); - int shareType = cursor.getInt(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_TYPE)); - navigateNoteShareDetail(shareWith, shareType); - } - return true; - } - }); - } - - private void navigateNoteShareDetail(String shareWith, int shareType) { - Bundle bundle = new Bundle(); - - bundle.putSerializable(NoteShareDetailActivity.ARG_NOTE, note); - bundle.putString(NoteShareDetailActivity.ARG_SHAREE_NAME, shareWith); - bundle.putInt(NoteShareDetailActivity.ARG_SHARE_TYPE, shareType); - - Intent intent = new Intent(this, NoteShareDetailActivity.class); - intent.putExtras(bundle); - resultLauncher.launch(intent); - } - - private boolean accountOwnsFile() { - if (shares.isEmpty()) { - return true; - } - - final var share = shares.get(0); - String ownerDisplayName = share.getOwnerDisplayName(); - return TextUtils.isEmpty(ownerDisplayName) || account.getAccountName().split("@")[0].equalsIgnoreCase(ownerDisplayName); - } - - private void setShareWithYou() { - if (accountOwnsFile()) { - binding.sharedWithYouContainer.setVisibility(View.GONE); - } else { - if (shares.isEmpty()) { - return; - } - - final var share = shares.get(0); - - binding.sharedWithYouUsername.setText( - String.format(getString(R.string.note_share_activity_shared_with_you), share.getOwnerDisplayName())); - AvatarLoader.INSTANCE.load(this, binding.sharedWithYouAvatar, account); - binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); - - String description = share.getNote(); - - if (!TextUtils.isEmpty(description)) { - binding.sharedWithYouNote.setText(description); - binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); - } else { - binding.sharedWithYouNoteContainer.setVisibility(View.GONE); - } - } - } - - public void copyInternalLink() { - if (account == null) { - DisplayUtils.showSnackMessage(this, getString(R.string.note_share_activity_could_not_retrieve_url)); - return; - } - - final var link = createInternalLink(); - showShareLinkDialog(link); - } - - @Override - public void createPublicShareLink() { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return; - } - - if (capabilities.getPublicPasswordEnforced() || capabilities.getAskForOptionalPassword()) { - // password enforced by server, request to the user before trying to create - requestPasswordForShareViaLink(true, capabilities.getAskForOptionalPassword()); - } else { - executorService.submit(() -> { - final var result = repository.addShare(note, ShareType.PUBLIC_LINK, ""); - runOnUiThread(() -> { - if (result instanceof ApiResult.Success> successResponse && binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, successResponse.getMessage()); - note.setIsShared(true); - repository.updateNote(note); - adapter.addShare(CreateShareResponseExtensionsKt.toOCShare(successResponse.getData().ocs.data)); - } else if (result instanceof ApiResult.Error errorResponse) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, errorResponse.getMessage()); - } - }); - }); - } - } - - public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(note, createShare, askForPassword, this); - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - // TODO: - @Override - public void createSecureFileDrop() { - - } - - private void showShareLinkDialog(String link) { - Intent intentToShareLink = new Intent(Intent.ACTION_SEND); - - intentToShareLink.putExtra(Intent.EXTRA_TEXT, link); - intentToShareLink.setType("text/plain"); - intentToShareLink.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.note_share_activity_subject_shared_with_you, note.getTitle())); - - String[] packagesToExclude = new String[]{this.getPackageName()}; - DialogFragment chooserDialog = ShareLinkToDialog.newInstance(intentToShareLink, packagesToExclude); - chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG); - } - - private String createInternalLink() { - Uri baseUri = Uri.parse(account.getUrl()); - return baseUri + "/index.php/f/" + note.getRemoteId(); - } - - @Override - public void copyLink(OCShare share) { - if (!note.isShared()) { - return; - } - - if (TextUtils.isEmpty(share.getShareLink())) { - copyAndShareFileLink(share.getShareLink()); - } else { - ClipboardUtil.copyToClipboard(this, share.getShareLink()); - } - } - - private void copyAndShareFileLink(String link) { - ClipboardUtil.copyToClipboard(this, link, false); - Snackbar snackbar = Snackbar - .make(this.findViewById(android.R.id.content), R.string.clipboard_text_copied, Snackbar.LENGTH_LONG) - .setAction(R.string.share, v -> showShareLinkDialog(link)); - snackbar.show(); - } - - @Override - public void showSharingMenuActionSheet(OCShare share) { - if (!this.isFinishing()) { - new NoteShareActivityShareItemActionBottomSheetDialog(this, this, share).show(); - } - } - - @Override - public void showShareExpirationSnackbar(OCShare share) { - String expirationDescription = getString( - R.string.share_expires, - SimpleDateFormat.getDateInstance().format(new Date(share.getExpirationDate())) - ); - Snackbar snackbar = Snackbar - .make(binding.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) - .setAction(R.string.dismiss, view -> { - // Clicking on empty action will dismiss snackbar - }); - final var util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this); - util.material.themeSnackbar(snackbar); - snackbar.show(); - } - - @Override - public void showPermissionsDialog(OCShare share) { - new QuickSharingPermissionsBottomSheetDialog(this, this, share).show(); - } - - @Override - public void requestPasswordForShare(OCShare share, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword, this); - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - // TODO: - @Override - public void showProfileBottomSheet(Account account, String shareWith) { - } - - private boolean isInitializingShares = false; - - public void updateShareeListAdapter() { - if (isInitializingShares) { - Log_OC.d(TAG, "Shares initialization already in progress"); - return; - } - - isInitializingShares = true; - - executorService.submit(() -> { - try { - ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter(); - - if (adapter == null) { - runOnUiThread(() -> DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.could_not_retrieve_shares))); - isInitializingShares = false; - return; - } - - List remoteNotes; - if (note != null) { - remoteNotes = repository.fetchSharesFromNote(note); - } else { - remoteNotes = new ArrayList<>(); - } - - runOnUiThread(() -> { - shares.clear(); - shares.addAll(remoteNotes); - - adapter.removeAll(); - adapter.addShares(shares); - addPublicShares(adapter); - setShareWithYou(); - - isInitializingShares = false; - }); - } catch (Exception e) { - Log_OC.d(TAG, "Exception while updateShareeListAdapter: " + e); - } - }); - } - - private void addPublicShares(ShareeListAdapter adapter) { - List publicShares = new ArrayList<>(); - - if (containsNoNewPublicShare(adapter.getShares())) { - final OCShare ocShare = new OCShare(); - ocShare.setShareType(ShareType.NEW_PUBLIC_LINK); - publicShares.add(ocShare); - } else { - adapter.removeNewPublicShare(); - } - - adapter.addShares(publicShares); - } - - private void checkContactPermission() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { - pickContactEmail(); - } else { - requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS); - } - } - - private void pickContactEmail() { - Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI); - - // FIXME: - if (intent.resolveActivity(getPackageManager()) != null) { - onContactSelectionResultLauncher.launch(intent); - } else { - DisplayUtils.showSnackMessage(this, getString(R.string.file_detail_sharing_fragment_no_contact_app_message)); - } - } - - private void handleContactResult(@NonNull Uri contactUri) { - // Define the projection to get all email addresses. - String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS}; - - Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null); - if (cursor == null) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address as Cursor is null."); - return; - } - - if (!cursor.moveToFirst()) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address as no Email found."); - return; - } - - // The contact has only one email address, use it. - int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS); - if (columnIndex == -1) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address."); - return; - } - - // Use the email address as needed. - // email variable contains the selected contact's email address. - String email = cursor.getString(columnIndex); - binding.searchView.post(() -> { - binding.searchView.setQuery(email, false); - binding.searchView.requestFocus(); - }); - - cursor.close(); - } - - private boolean containsNoNewPublicShare(List shares) { - for (OCShare share : shares) { - if (share.getShareType() != null && share.getShareType() == ShareType.NEW_PUBLIC_LINK) { - return false; - } - } - - return true; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(ARG_NOTE, note); - outState.putSerializable(ARG_ACCOUNT, account); - } - - private boolean isReshareForbidden(OCShare share) { - return (share.getShareType() != null && ShareType.FEDERATED == share.getShareType()) || - capabilities != null && !capabilities.isReSharingAllowed(); - } - - @VisibleForTesting - public void search(String query) { - SearchView searchView = findViewById(R.id.searchView); - searchView.setQuery(query, true); - } - - @Override - public void advancedPermissions(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION, false); - } - - - @Override - public void sendNewEmail(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE, true); - } - - @Override - public void unShare(OCShare share) { - executorService.submit(() -> { - final var result = repository.removeShare(share, note); - - runOnUiThread(() -> { - if (result) { - ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter(); - if (adapter == null) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.email_pick_failed)); - return; - } - adapter.remove(share); - } else { - DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.failed_the_remove_share)); - } - }); - }); - } - - // TODO: - @Override - public void sendLink(OCShare share) { - } - - @Override - public void addAnotherLink(OCShare share) { - createPublicShareLink(); - } - - private void modifyExistingShare(OCShare share, int screenTypePermission, boolean sendEmail) { - Bundle bundle = new Bundle(); - - bundle.putSerializable(NoteShareDetailActivity.ARG_OCSHARE, share); - bundle.putInt(NoteShareDetailActivity.ARG_SCREEN_TYPE, screenTypePermission); - bundle.putBoolean(NoteShareDetailActivity.ARG_RESHARE_SHOWN, !isReshareForbidden(share)); - bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, getExpDateShown()); - bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail); - - Intent intent = new Intent(this, NoteShareDetailActivity.class); - intent.putExtras(bundle); - resultLauncher.launch(intent); - } - - private boolean getExpDateShown() { - try { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return false; - } - - final var majorVersionAsString = capabilities.getNextcloudMajorVersion(); - if (majorVersionAsString != null) { - final var majorVersion = Integer.parseInt(majorVersionAsString); - return majorVersion >= 18; - } - - return false; - } catch (NumberFormatException e) { - Log_OC.d(TAG, "Exception while getting expDateShown"); - return false; - } - } - - @Override - public void onQuickPermissionChanged(OCShare share, int permission) { - executorService.submit(() -> { - final var result = repository.updateSharePermission(share.getId(), permission); - runOnUiThread(() -> { - if (ApiResultKt.isSuccess(result)) { - updateShare(share); - } else if (result instanceof ApiResult.Error error) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - }); - } - - private void updateShare(OCShare share) { - if (note == null) { - Log_OC.e(TAG, "note is null, cannot update share"); - return; - } - - executorService.submit(() -> { - try { - final var updatedShares = repository.fetchSharesFromNote(note); - - runOnUiThread(() -> { - if (binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - OCShare updatedShare = null; - for (int i=0;i requestContactPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - pickContactEmail(); - } else { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.note_share_activity_contact_no_permission), Snackbar.LENGTH_LONG) - .show(); - } - }); - - private final ActivityResultLauncher onContactSelectionResultLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent intent = result.getData(); - if (intent == null) { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.email_pick_failed), Snackbar.LENGTH_LONG) - .show(); - return; - } - - Uri contactUri = intent.getData(); - if (contactUri == null) { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.email_pick_failed), Snackbar.LENGTH_LONG) - .show(); - return; - } - - handleContactResult(contactUri); - - } - }); - - @Override - public void applyBrand(int color) { - final var util = BrandingUtil.of(color, this); - util.platform.themeStatusBar(this); - util.material.themeToolbar(binding.toolbar); - util.notes.themeContentSearchView(binding.searchView); - util.platform.colorImageView(binding.searchViewIcon, ColorRole.ON_SURFACE_VARIANT); - util.platform.colorImageView(binding.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT); - util.platform.colorCircularProgressBar(binding.loadingLayoutIndicator, ColorRole.PRIMARY); - util.platform.themeHorizontalProgressBar(binding.progressBar); - util.platform.colorViewBackground(getWindow().getDecorView()); - util.platform.colorViewBackground(binding.getRoot()); - util.material.colorMaterialButtonPrimaryOutlined(binding.btnShareButton); - util.platform.colorTextView(binding.title, ColorRole.ON_SURFACE); - util.platform.colorTextView(binding.fileName, ColorRole.ON_SURFACE_VARIANT); - util.notes.themeSearchCardView(binding.searchCardWrapper); - } - - @Override - protected void onDestroy() { - executorService.shutdown(); - super.onDestroy(); - } - - @Override - public void shareFileViaPublicShare(@Nullable Note note, @Nullable String password) { - if (note == null || password == null) { - Log_OC.d(TAG, "note or password is null, cannot create a public share"); - return; - } - - executorService.submit(() -> { - final var result = repository.addShare( - note, - ShareType.PUBLIC_LINK, - "", - "false", - password, - repository.getCapabilities().getDefaultPermission() - ); - - runOnUiThread(() -> { - if (result instanceof ApiResult.Success> successResponse && - binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - adapter.addShare(CreateShareResponseExtensionsKt.toOCShare(successResponse.getData().ocs.data)); - } else if (result instanceof ApiResult.Error error) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - }); - } - - @Override - public void setPasswordToShare(@NotNull OCShare share, @Nullable String password) { - if (password == null) { - Log_OC.d(TAG, "password is null, cannot update a public share"); - return; - } - - executorService.submit(() -> { - { - boolean isDownloadAndAllowsSyncEnabled = repository.isAllowDownloadAndSync(share); - - String attributes = UpdateShareRequest.Companion.createAttributes( - repository.getCapabilities(), - isDownloadAndAllowsSyncEnabled, - share.getShareType() - ); - - final var requestBody = new UpdateShareRequest( - share.getPermissions(), - password, - share.getNote(), - share.getLabel(), - DateUtil.INSTANCE.getExpirationDate(share.getExpirationDate()), - Boolean.toString(share.isHideFileDownload()), - attributes - ); - final var result = repository.updateShare(share.getId(), requestBody); - - runOnUiThread(() -> { - if (ApiResultKt.isSuccess(result)) { - final var intent = new Intent(NoteShareActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - NoteShareActivity.this.startActivity(intent); - } else if (ApiResultKt.isError(result)) { - ApiResult.Error error = (ApiResult.Error) result; - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - } - }); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt new file mode 100644 index 000000000..8db385bc1 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt @@ -0,0 +1,945 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.share + +import android.Manifest +import android.app.Activity +import android.app.SearchManager +import android.content.ComponentName +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.ContactsContract +import android.text.InputType +import android.text.TextUtils +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.BrandedActivity +import it.niedermann.owncloud.notes.branding.BrandedSnackbar +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding +import it.niedermann.owncloud.notes.main.MainActivity +import it.niedermann.owncloud.notes.persistence.ApiResult +import it.niedermann.owncloud.notes.persistence.entity.Account +import it.niedermann.owncloud.notes.persistence.entity.Note +import it.niedermann.owncloud.notes.persistence.isError +import it.niedermann.owncloud.notes.persistence.isSuccess +import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter +import it.niedermann.owncloud.notes.share.adapter.SuggestionAdapter +import it.niedermann.owncloud.notes.share.dialog.NoteShareActivityShareItemActionBottomSheetDialog +import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog +import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions +import it.niedermann.owncloud.notes.share.dialog.ShareLinkToDialog.Companion.newInstance +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment.Companion.newInstance +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment.SharePasswordDialogListener +import it.niedermann.owncloud.notes.share.helper.AvatarLoader +import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider +import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction +import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener +import it.niedermann.owncloud.notes.share.model.CreateShareResponse +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest.Companion.createAttributes +import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig.reset +import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig.searchOnlyUsers +import it.niedermann.owncloud.notes.share.model.toOCShare +import it.niedermann.owncloud.notes.share.repository.ShareRepository +import it.niedermann.owncloud.notes.shared.model.Capabilities +import it.niedermann.owncloud.notes.shared.model.OcsResponse +import it.niedermann.owncloud.notes.shared.util.DisplayUtils +import it.niedermann.owncloud.notes.shared.util.ShareUtil +import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil.copyToClipboard +import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument +import it.niedermann.owncloud.notes.util.DateUtil.getExpirationDate +import java.text.SimpleDateFormat +import java.util.Collections +import java.util.Date +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShareItemAction, + QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { + private var executorService: ScheduledExecutorService? = null + private var future: Future<*>? = null + private var binding: ActivityNoteShareBinding? = null + private var note: Note? = null + private var account: Account? = null + private var repository: ShareRepository? = null + private var capabilities: Capabilities? = null + private var resultLauncher: ActivityResultLauncher? = null + private val shares: MutableList = + Collections.synchronizedList(ArrayList()) + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + executorService = Executors.newSingleThreadScheduledExecutor() + binding = ActivityNoteShareBinding.inflate(getLayoutInflater()) + setContentView(binding!!.getRoot()) + setSupportActionBar(binding!!.toolbar) + binding!!.toolbar.setNavigationOnClickListener(View.OnClickListener { v: View? -> backPressed() }) + registerResultLauncher() + initializeArguments() + initializeOnBackPressedDispatcher() + } + + private fun registerResultLauncher() { + resultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ActivityResultCallback { result: ActivityResult? -> + if (result!!.getResultCode() == RESULT_OK && result.getData() != null) { + recreate() + } + }) + } + + private fun initializeOnBackPressedDispatcher() { + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + backPressed() + } + }) + } + + private fun backPressed() { + val intent = Intent(this@NoteShareActivity, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + finish() + } + + private fun initializeArguments() { + val bundler = getIntent().getExtras() + note = bundler.getSerializableArgument(ARG_NOTE, Note::class.java) + account = bundler.getSerializableArgument(ARG_ACCOUNT, Account::class.java) + + requireNotNull(note) { "Note cannot be null" } + + requireNotNull(account) { "Account cannot be null" } + + executorService!!.submit(Runnable { + try { + val ssoAcc = + SingleAccountHelper.getCurrentSingleSignOnAccount(this@NoteShareActivity) + repository = ShareRepository(this@NoteShareActivity, ssoAcc) + capabilities = repository!!.getCapabilities() + repository!!.fetchSharesForNotesAndSaveShareEntities() + + runOnUiThread(Runnable { + binding!!.fileName.setText(note!!.getTitle()) + binding!!.searchContainer.setVisibility(View.VISIBLE) + binding!!.sharesList.setVisibility(View.VISIBLE) + binding!!.sharesList.setAdapter( + ShareeListAdapter( + this, + ArrayList(), + this, + account + ) + ) + binding!!.sharesList.setLayoutManager(LinearLayoutManager(this)) + binding!!.pickContactEmailBtn.setOnClickListener(View.OnClickListener { v: View? -> checkContactPermission() }) + binding!!.btnShareButton.setOnClickListener(View.OnClickListener { v: View? -> + ShareUtil.openShareDialog( + this, + note!!.getTitle(), + note!!.getContent() + ) + }) + + if (note!!.getReadonly()) { + setupReadOnlySearchView() + } else { + setupSearchView( + getSystemService(SEARCH_SERVICE) as SearchManager?, + getComponentName() + ) + } + + updateShareeListAdapter() + binding!!.loadingLayout.setVisibility(View.GONE) + }) + } catch (e: Exception) { + Log_OC.e(TAG, "Exception at NoteShareActivity.init: " + e) + } + }) + } + + public override fun onStart() { + super.onStart() + + // note cannot be encrypted - logic ported from files app + searchOnlyUsers = false + } + + public override fun onStop() { + super.onStop() + reset() + } + + private fun disableSearchView(view: View) { + view.setEnabled(false) + + if (view is ViewGroup) { + for (i in 0..(androidx.appcompat.R.id.search_close_btn) + closeButton.setOnClickListener(View.OnClickListener { v: View? -> + binding!!.progressBar.setVisibility(View.GONE) + binding!!.searchView.setQuery("", false) + }) + + binding!!.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + // return true to prevent the query from being processed; + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + if (!newText.isEmpty()) { + binding!!.progressBar.setVisibility(View.VISIBLE) + + // Cancel the previous task if it's still running + if (future != null && !future!!.isDone()) { + future!!.cancel(true) + } + + // Schedule a new task with a delay + future = executorService!!.schedule(Runnable schedule@{ + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return@schedule + } + val isFederationShareAllowed = capabilities!!.getFederationShare() + try { + val cursor = + provider.searchForUsersOrGroups(newText, isFederationShareAllowed) + + if (cursor == null || cursor.getCount() == 0) { + return@schedule + } + + runOnUiThread(Runnable { + if (binding!!.searchView.getVisibility() == View.VISIBLE) { + suggestionAdapter.swapCursor(cursor) + } + binding!!.progressBar.setVisibility(View.GONE) + }) + } catch (e: Exception) { + Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: " + e) + runOnUiThread(Runnable { binding!!.progressBar.setVisibility(View.GONE) }) + } + }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS) + } else { + binding!!.progressBar.setVisibility(View.GONE) + suggestionAdapter.swapCursor(null) + } + return false + } + }) + + binding!!.searchView.setOnQueryTextFocusChangeListener(OnFocusChangeListener { v: View?, hasFocus: Boolean -> + if (hasFocus) { + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput( + binding!!.searchView.findFocus(), + InputMethodManager.SHOW_IMPLICIT + ) + } + }) + + binding!!.searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { + override fun onSuggestionSelect(position: Int): Boolean { + return false + } + + override fun onSuggestionClick(position: Int): Boolean { + val cursor = suggestionAdapter.getCursor() + if (cursor != null && cursor.moveToPosition(position)) { + val suggestion = + cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)) + binding!!.searchView.setQuery(suggestion, false) + + val shareWith = + cursor.getString(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_WITH)) + val shareType = + cursor.getInt(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_TYPE)) + navigateNoteShareDetail(shareWith, shareType) + } + return true + } + }) + } + + private fun navigateNoteShareDetail(shareWith: String?, shareType: Int) { + val bundle = Bundle() + + bundle.putSerializable(NoteShareDetailActivity.ARG_NOTE, note) + bundle.putString(NoteShareDetailActivity.ARG_SHAREE_NAME, shareWith) + bundle.putInt(NoteShareDetailActivity.ARG_SHARE_TYPE, shareType) + + val intent = Intent(this, NoteShareDetailActivity::class.java) + intent.putExtras(bundle) + resultLauncher!!.launch(intent) + } + + private fun accountOwnsFile(): Boolean { + if (shares.isEmpty()) { + return true + } + + val share = shares.get(0) + val ownerDisplayName = share.ownerDisplayName + return TextUtils.isEmpty(ownerDisplayName) || account!!.getAccountName() + .split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].equals( + ownerDisplayName, + ignoreCase = true + ) + } + + private fun setShareWithYou() { + if (accountOwnsFile()) { + binding!!.sharedWithYouContainer.setVisibility(View.GONE) + } else { + if (shares.isEmpty()) { + return + } + + val share = shares.get(0) + + binding!!.sharedWithYouUsername.setText( + String.format( + getString(R.string.note_share_activity_shared_with_you), + share.ownerDisplayName + ) + ) + AvatarLoader.load(this, binding!!.sharedWithYouAvatar, account!!) + binding!!.sharedWithYouAvatar.setVisibility(View.VISIBLE) + + val description = share.note + + if (!TextUtils.isEmpty(description)) { + binding!!.sharedWithYouNote.setText(description) + binding!!.sharedWithYouNoteContainer.setVisibility(View.VISIBLE) + } else { + binding!!.sharedWithYouNoteContainer.setVisibility(View.GONE) + } + } + } + + override fun copyInternalLink() { + if (account == null) { + DisplayUtils.showSnackMessage( + this, + getString(R.string.note_share_activity_could_not_retrieve_url) + ) + return + } + + val link = createInternalLink() + showShareLinkDialog(link) + } + + override fun createPublicShareLink() { + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return + } + + if (capabilities!!.getPublicPasswordEnforced() || capabilities!!.getAskForOptionalPassword()) { + // password enforced by server, request to the user before trying to create + requestPasswordForShareViaLink(true, capabilities!!.getAskForOptionalPassword()) + } else { + executorService!!.submit(Runnable { + val result: ApiResult?> = + repository!!.addShare(note!!, ShareType.PUBLIC_LINK, "") + runOnUiThread(Runnable { + if (result is ApiResult.Success?> && binding!!.sharesList.getAdapter() is ShareeListAdapter) { + DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + note!!.setIsShared(true) + repository!!.updateNote(note!!) + val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + adapter.addShare(result.data!!.ocs.data!!.toOCShare()) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + } + }) + }) + } + } + + fun requestPasswordForShareViaLink(createShare: Boolean, askForPassword: Boolean) { + val dialog = newInstance(note, createShare, askForPassword, this) + dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT) + } + + // TODO: + override fun createSecureFileDrop() { + } + + private fun showShareLinkDialog(link: String?) { + val intentToShareLink = Intent(Intent.ACTION_SEND) + + intentToShareLink.putExtra(Intent.EXTRA_TEXT, link) + intentToShareLink.setType("text/plain") + intentToShareLink.putExtra( + Intent.EXTRA_SUBJECT, + getString(R.string.note_share_activity_subject_shared_with_you, note!!.getTitle()) + ) + + val packagesToExclude = arrayOf(this.getPackageName()) + val chooserDialog: DialogFragment = newInstance(intentToShareLink, *packagesToExclude) + chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG) + } + + private fun createInternalLink(): String { + val baseUri = Uri.parse(account!!.getUrl()) + return baseUri.toString() + "/index.php/f/" + note!!.getRemoteId() + } + + override fun copyLink(share: OCShare) { + if (!note!!.isShared()) { + return + } + + if (TextUtils.isEmpty(share.shareLink)) { + copyAndShareFileLink(share.shareLink) + } else { + copyToClipboard(this, share.shareLink) + } + } + + private fun copyAndShareFileLink(link: String?) { + copyToClipboard(this, link, false) + val snackbar = Snackbar + .make( + this.findViewById(android.R.id.content)!!, + R.string.clipboard_text_copied, + Snackbar.LENGTH_LONG + ) + .setAction( + R.string.share, + View.OnClickListener { v: View? -> showShareLinkDialog(link) }) + snackbar.show() + } + + override fun showSharingMenuActionSheet(share: OCShare?) { + if (!this.isFinishing()) { + NoteShareActivityShareItemActionBottomSheetDialog(this, this, share).show() + } + } + + override fun showShareExpirationSnackbar(share: OCShare) { + val expirationDescription = getString( + R.string.share_expires, + SimpleDateFormat.getDateInstance().format(Date(share.expirationDate)) + ) + val snackbar = Snackbar + .make(binding!!.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) + .setAction(R.string.dismiss, View.OnClickListener { view: View? -> }) + val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this) + util.material.themeSnackbar(snackbar) + snackbar.show() + } + + override fun showPermissionsDialog(share: OCShare?) { + QuickSharingPermissionsBottomSheetDialog(this, this, share).show() + } + + override fun requestPasswordForShare(share: OCShare?, askForPassword: Boolean) { + val dialog = newInstance(share, askForPassword, this) + dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT) + } + + // TODO: + override fun showProfileBottomSheet(account: Account?, shareWith: String?) { + } + + private var isInitializingShares = false + + fun updateShareeListAdapter() { + if (isInitializingShares) { + Log_OC.d(TAG, "Shares initialization already in progress") + return + } + + isInitializingShares = true + + executorService!!.submit(Runnable submit@{ + try { + val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter? + + if (adapter == null) { + runOnUiThread(Runnable { + DisplayUtils.showSnackMessage( + this@NoteShareActivity, + getString(R.string.could_not_retrieve_shares) + ) + }) + isInitializingShares = false + return@submit + } + + val remoteNotes: List + if (note != null) { + remoteNotes = repository!!.fetchSharesFromNote(note!!) + } else { + remoteNotes = listOf() + } + + runOnUiThread(Runnable { + shares.clear() + shares.addAll(remoteNotes) + + adapter.removeAll() + adapter.addShares(shares) + addPublicShares(adapter) + setShareWithYou() + isInitializingShares = false + }) + } catch (e: Exception) { + Log_OC.d(TAG, "Exception while updateShareeListAdapter: " + e) + } + }) + } + + private fun addPublicShares(adapter: ShareeListAdapter) { + val publicShares: MutableList = ArrayList() + + if (containsNoNewPublicShare(adapter.getShares())) { + val ocShare = OCShare() + ocShare.shareType = ShareType.NEW_PUBLIC_LINK + publicShares.add(ocShare) + } else { + adapter.removeNewPublicShare() + } + + adapter.addShares(publicShares) + } + + private fun checkContactPermission() { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + ) { + pickContactEmail() + } else { + requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS) + } + } + + private fun pickContactEmail() { + val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) + + // FIXME: + if (intent.resolveActivity(getPackageManager()) != null) { + onContactSelectionResultLauncher.launch(intent) + } else { + DisplayUtils.showSnackMessage( + this, + getString(R.string.file_detail_sharing_fragment_no_contact_app_message) + ) + } + } + + private fun handleContactResult(contactUri: Uri) { + // Define the projection to get all email addresses. + val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS) + + val cursor = getContentResolver().query(contactUri, projection, null, null, null) + if (cursor == null) { + DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address as Cursor is null.") + return + } + + if (!cursor.moveToFirst()) { + DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address as no Email found.") + return + } + + // The contact has only one email address, use it. + val columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + if (columnIndex == -1) { + DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address.") + return + } + + // Use the email address as needed. + // email variable contains the selected contact's email address. + val email = cursor.getString(columnIndex) + binding!!.searchView.post(Runnable { + binding!!.searchView.setQuery(email, false) + binding!!.searchView.requestFocus() + }) + + cursor.close() + } + + private fun containsNoNewPublicShare(shares: MutableList): Boolean { + for (share in shares) { + if (share.shareType != null && share.shareType == ShareType.NEW_PUBLIC_LINK) { + return false + } + } + + return true + } + + public override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(ARG_NOTE, note) + outState.putSerializable(ARG_ACCOUNT, account) + } + + private fun isReshareForbidden(share: OCShare): Boolean { + return (share.shareType != null && ShareType.FEDERATED == share.shareType) || + capabilities != null && !capabilities!!.isReSharingAllowed() + } + + @VisibleForTesting + fun search(query: String?) { + val searchView = findViewById(R.id.searchView) + searchView.setQuery(query, true) + } + + override fun advancedPermissions(share: OCShare) { + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION, false) + } + + + override fun sendNewEmail(share: OCShare) { + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE, true) + } + + override fun unShare(share: OCShare) { + executorService!!.submit(Runnable { + val result = repository!!.removeShare(share, note!!) + runOnUiThread(Runnable { + if (result) { + val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter? + if (adapter == null) { + DisplayUtils.showSnackMessage( + this@NoteShareActivity, + getString(R.string.email_pick_failed) + ) + return@Runnable + } + adapter.remove(share) + } else { + DisplayUtils.showSnackMessage( + this@NoteShareActivity, + getString(R.string.failed_the_remove_share) + ) + } + }) + }) + } + + // TODO: + override fun sendLink(share: OCShare?) { + } + + override fun addAnotherLink(share: OCShare?) { + createPublicShareLink() + } + + private fun modifyExistingShare(share: OCShare, screenTypePermission: Int, sendEmail: Boolean) { + val bundle = Bundle() + + bundle.putSerializable(NoteShareDetailActivity.ARG_OCSHARE, share) + bundle.putInt(NoteShareDetailActivity.ARG_SCREEN_TYPE, screenTypePermission) + bundle.putBoolean(NoteShareDetailActivity.ARG_RESHARE_SHOWN, !isReshareForbidden(share)) + bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, this.expDateShown) + bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail) + + val intent = Intent(this, NoteShareDetailActivity::class.java) + intent.putExtras(bundle) + resultLauncher!!.launch(intent) + } + + private val expDateShown: Boolean + get() { + try { + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return false + } + + val majorVersionAsString = capabilities!!.getNextcloudMajorVersion() + if (majorVersionAsString != null) { + val majorVersion = majorVersionAsString.toInt() + return majorVersion >= 18 + } + + return false + } catch (e: NumberFormatException) { + Log_OC.d(TAG, "Exception while getting expDateShown") + return false + } + } + + override fun onQuickPermissionChanged(share: OCShare, permission: Int) { + executorService!!.submit(Runnable { + val result: ApiResult?> = + repository!!.updateSharePermission(share.id, permission) + runOnUiThread(Runnable { + if (result.isSuccess?>()) { + updateShare(share) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + } + }) + }) + } + + private fun updateShare(share: OCShare) { + if (note == null) { + Log_OC.e(TAG, "note is null, cannot update share") + return + } + + executorService!!.submit(Runnable { + try { + val updatedShares: List = repository!!.fetchSharesFromNote(note!!) + + runOnUiThread(Runnable { + if (binding!!.sharesList.getAdapter() is ShareeListAdapter) { + var updatedShare: OCShare? = null + for (i in updatedShares.indices) { + if (updatedShares.get(i)!!.id == share.id) { + updatedShare = updatedShares.get(i) + break + } + } + + if (updatedShare == null) { + return@Runnable + } + + val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + adapter.updateShare(updatedShare) + } + }) + } catch (e: Exception) { + Log_OC.d(TAG, "Exception while refreshing share info: " + e) + } + }) + } + + private val requestContactPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), ActivityResultCallback { isGranted: Boolean? -> + if (isGranted == true) { + pickContactEmail() + } else { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.note_share_activity_contact_no_permission), + Snackbar.LENGTH_LONG + ) + .show() + } + }) + + private val onContactSelectionResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ActivityResultCallback { result: ActivityResult? -> + if (result!!.getResultCode() == RESULT_OK) { + val intent = result.getData() + if (intent == null) { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.email_pick_failed), + Snackbar.LENGTH_LONG + ) + .show() + return@ActivityResultCallback + } + + val contactUri = intent.getData() + if (contactUri == null) { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.email_pick_failed), + Snackbar.LENGTH_LONG + ) + .show() + return@ActivityResultCallback + } + + handleContactResult(contactUri) + } + }) + + override fun applyBrand(color: Int) { + val util = BrandingUtil.of(color, this) + util.platform.themeStatusBar(this) + util.material.themeToolbar(binding!!.toolbar) + util.notes.themeContentSearchView(binding!!.searchView) + util.platform.colorImageView(binding!!.searchViewIcon, ColorRole.ON_SURFACE_VARIANT) + util.platform.colorImageView(binding!!.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT) + util.platform.colorCircularProgressBar(binding!!.loadingLayoutIndicator, ColorRole.PRIMARY) + util.platform.themeHorizontalProgressBar(binding!!.progressBar) + util.platform.colorViewBackground(getWindow().getDecorView()) + util.platform.colorViewBackground(binding!!.getRoot()) + util.material.colorMaterialButtonPrimaryOutlined(binding!!.btnShareButton) + util.platform.colorTextView(binding!!.title, ColorRole.ON_SURFACE) + util.platform.colorTextView(binding!!.fileName, ColorRole.ON_SURFACE_VARIANT) + util.notes.themeSearchCardView(binding!!.searchCardWrapper) + } + + override fun onDestroy() { + executorService!!.shutdown() + super.onDestroy() + } + + override fun shareFileViaPublicShare(note: Note?, password: String?) { + if (note == null || password == null) { + Log_OC.d(TAG, "note or password is null, cannot create a public share") + return + } + + executorService!!.submit(Runnable { + val result: ApiResult?> = repository!!.addShare( + note, + ShareType.PUBLIC_LINK, + "", + "false", + password, + repository!!.getCapabilities().getDefaultPermission() + ) + runOnUiThread(Runnable { + if (result is ApiResult.Success?> && + binding!!.sharesList.getAdapter() is ShareeListAdapter + ) { + val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + adapter.addShare(result.data!!.ocs.data!!.toOCShare()) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + } + }) + }) + } + + override fun setPasswordToShare(share: OCShare, password: String?) { + if (password == null) { + Log_OC.d(TAG, "password is null, cannot update a public share") + return + } + + executorService!!.submit(Runnable { + run { + val isDownloadAndAllowsSyncEnabled = repository!!.isAllowDownloadAndSync(share) + val attributes = createAttributes( + repository!!.getCapabilities(), + isDownloadAndAllowsSyncEnabled, + share.shareType + ) + + val requestBody = UpdateShareRequest( + share.permissions, + password, + share.note, + share.label, + getExpirationDate(share.expirationDate), + share.isHideFileDownload.toString(), + attributes + ) + val result: ApiResult?> = + repository!!.updateShare(share.id, requestBody) + runOnUiThread(Runnable { + if (result.isSuccess?>()) { + val intent = Intent(this@NoteShareActivity, MainActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + this@NoteShareActivity.startActivity(intent) + } else if (result.isError?>()) { + val error = result as ApiResult.Error + DisplayUtils.showSnackMessage(this@NoteShareActivity, error.message) + } + }) + } + }) + } + + companion object { + private const val TAG = "NoteShareActivity" + const val ARG_NOTE: String = "NOTE" + const val ARG_ACCOUNT: String = "ACCOUNT" + const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" + + private const val SEARCH_DELAY_MS: Long = 500 + + @JvmStatic + fun getActivityStartIntent(note: Note, account: Account, activity: Activity): Intent { + val bundle = Bundle() + bundle.putSerializable(ARG_NOTE, note) + bundle.putSerializable(ARG_ACCOUNT, account) + + val intent = Intent(activity, NoteShareActivity::class.java) + intent.putExtras(bundle) + return intent + } + } +} From 0ea331447e944218ee12c86f26be28bc9ba37275 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 17:30:32 +0200 Subject: [PATCH 08/96] Refactored NoteShareActivity to be more kotlin idiomatic Signed-off-by: daniele-verducci --- .../owncloud/notes/share/NoteShareActivity.kt | 283 +++++++++--------- 1 file changed, 141 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt index 8db385bc1..146ada1b7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt @@ -18,7 +18,6 @@ import android.provider.ContactsContract import android.text.InputType import android.text.TextUtils import android.view.View -import android.view.View.OnFocusChangeListener import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager @@ -83,6 +82,9 @@ import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit +import androidx.core.view.size +import androidx.core.view.isVisible +import androidx.core.net.toUri class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShareItemAction, QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { @@ -101,10 +103,10 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar super.onCreate(savedInstanceState) executorService = Executors.newSingleThreadScheduledExecutor() - binding = ActivityNoteShareBinding.inflate(getLayoutInflater()) + binding = ActivityNoteShareBinding.inflate(layoutInflater) setContentView(binding!!.getRoot()) setSupportActionBar(binding!!.toolbar) - binding!!.toolbar.setNavigationOnClickListener(View.OnClickListener { v: View? -> backPressed() }) + binding!!.toolbar.setNavigationOnClickListener { backPressed() } registerResultLauncher() initializeArguments() initializeOnBackPressedDispatcher() @@ -112,12 +114,12 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private fun registerResultLauncher() { resultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ActivityResultCallback { result: ActivityResult? -> - if (result!!.getResultCode() == RESULT_OK && result.getData() != null) { - recreate() - } - }) + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult? -> + if (result!!.resultCode == RESULT_OK && result.data != null) { + recreate() + } + } } private fun initializeOnBackPressedDispatcher() { @@ -136,7 +138,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar } private fun initializeArguments() { - val bundler = getIntent().getExtras() + val bundler = intent.extras note = bundler.getSerializableArgument(ARG_NOTE, Note::class.java) account = bundler.getSerializableArgument(ARG_ACCOUNT, Account::class.java) @@ -144,7 +146,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar requireNotNull(account) { "Account cannot be null" } - executorService!!.submit(Runnable { + executorService!!.submit { try { val ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(this@NoteShareActivity) @@ -152,10 +154,10 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar capabilities = repository!!.getCapabilities() repository!!.fetchSharesForNotesAndSaveShareEntities() - runOnUiThread(Runnable { - binding!!.fileName.setText(note!!.getTitle()) - binding!!.searchContainer.setVisibility(View.VISIBLE) - binding!!.sharesList.setVisibility(View.VISIBLE) + runOnUiThread { + binding!!.fileName.text = note!!.title + binding!!.searchContainer.visibility = View.VISIBLE + binding!!.sharesList.visibility = View.VISIBLE binding!!.sharesList.setAdapter( ShareeListAdapter( this, @@ -165,31 +167,31 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar ) ) binding!!.sharesList.setLayoutManager(LinearLayoutManager(this)) - binding!!.pickContactEmailBtn.setOnClickListener(View.OnClickListener { v: View? -> checkContactPermission() }) - binding!!.btnShareButton.setOnClickListener(View.OnClickListener { v: View? -> + binding!!.pickContactEmailBtn.setOnClickListener { checkContactPermission() } + binding!!.btnShareButton.setOnClickListener { ShareUtil.openShareDialog( this, - note!!.getTitle(), - note!!.getContent() + note!!.title, + note!!.content ) - }) + } - if (note!!.getReadonly()) { + if (note!!.readonly) { setupReadOnlySearchView() } else { setupSearchView( getSystemService(SEARCH_SERVICE) as SearchManager?, - getComponentName() + componentName ) } updateShareeListAdapter() - binding!!.loadingLayout.setVisibility(View.GONE) - }) + binding!!.loadingLayout.visibility = View.GONE + } } catch (e: Exception) { - Log_OC.e(TAG, "Exception at NoteShareActivity.init: " + e) + Log_OC.e(TAG, "Exception at NoteShareActivity.init: $e") } - }) + } } public override fun onStart() { @@ -208,7 +210,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar view.setEnabled(false) if (view is ViewGroup) { - for (i in 0..(androidx.appcompat.R.id.search_close_btn) - closeButton.setOnClickListener(View.OnClickListener { v: View? -> - binding!!.progressBar.setVisibility(View.GONE) + closeButton.setOnClickListener { + binding!!.progressBar.visibility = View.GONE binding!!.searchView.setQuery("", false) - }) + } binding!!.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -254,11 +256,11 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar } override fun onQueryTextChange(newText: String): Boolean { - if (!newText.isEmpty()) { - binding!!.progressBar.setVisibility(View.VISIBLE) + if (newText.isNotEmpty()) { + binding!!.progressBar.visibility = View.VISIBLE // Cancel the previous task if it's still running - if (future != null && !future!!.isDone()) { + if (future != null && !future!!.isDone) { future!!.cancel(true) } @@ -268,35 +270,35 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar Log_OC.d(TAG, "Capabilities cannot be null") return@schedule } - val isFederationShareAllowed = capabilities!!.getFederationShare() + val isFederationShareAllowed = capabilities!!.federationShare try { val cursor = provider.searchForUsersOrGroups(newText, isFederationShareAllowed) - if (cursor == null || cursor.getCount() == 0) { + if (cursor == null || cursor.count == 0) { return@schedule } - runOnUiThread(Runnable { - if (binding!!.searchView.getVisibility() == View.VISIBLE) { + runOnUiThread { + if (binding!!.searchView.isVisible) { suggestionAdapter.swapCursor(cursor) } - binding!!.progressBar.setVisibility(View.GONE) - }) + binding!!.progressBar.visibility = View.GONE + } } catch (e: Exception) { - Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: " + e) - runOnUiThread(Runnable { binding!!.progressBar.setVisibility(View.GONE) }) + Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: $e") + runOnUiThread { binding!!.progressBar.visibility = View.GONE } } }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS) } else { - binding!!.progressBar.setVisibility(View.GONE) + binding!!.progressBar.visibility = View.GONE suggestionAdapter.swapCursor(null) } return false } }) - binding!!.searchView.setOnQueryTextFocusChangeListener(OnFocusChangeListener { v: View?, hasFocus: Boolean -> + binding!!.searchView.setOnQueryTextFocusChangeListener { _: View?, hasFocus: Boolean -> if (hasFocus) { val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput( @@ -304,7 +306,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar InputMethodManager.SHOW_IMPLICIT ) } - }) + } binding!!.searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { override fun onSuggestionSelect(position: Int): Boolean { @@ -312,7 +314,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar } override fun onSuggestionClick(position: Int): Boolean { - val cursor = suggestionAdapter.getCursor() + val cursor = suggestionAdapter.cursor if (cursor != null && cursor.moveToPosition(position)) { val suggestion = cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)) @@ -346,9 +348,9 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return true } - val share = shares.get(0) + val share = shares[0] val ownerDisplayName = share.ownerDisplayName - return TextUtils.isEmpty(ownerDisplayName) || account!!.getAccountName() + return TextUtils.isEmpty(ownerDisplayName) || account!!.accountName .split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].equals( ownerDisplayName, ignoreCase = true @@ -357,19 +359,17 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private fun setShareWithYou() { if (accountOwnsFile()) { - binding!!.sharedWithYouContainer.setVisibility(View.GONE) + binding!!.sharedWithYouContainer.visibility = View.GONE } else { if (shares.isEmpty()) { return } - val share = shares.get(0) + val share = shares[0] - binding!!.sharedWithYouUsername.setText( - String.format( - getString(R.string.note_share_activity_shared_with_you), - share.ownerDisplayName - ) + binding!!.sharedWithYouUsername.text = String.format( + getString(R.string.note_share_activity_shared_with_you), + share.ownerDisplayName ) AvatarLoader.load(this, binding!!.sharedWithYouAvatar, account!!) binding!!.sharedWithYouAvatar.setVisibility(View.VISIBLE) @@ -378,9 +378,9 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar if (!TextUtils.isEmpty(description)) { binding!!.sharedWithYouNote.setText(description) - binding!!.sharedWithYouNoteContainer.setVisibility(View.VISIBLE) + binding!!.sharedWithYouNoteContainer.visibility = View.VISIBLE } else { - binding!!.sharedWithYouNoteContainer.setVisibility(View.GONE) + binding!!.sharedWithYouNoteContainer.visibility = View.GONE } } } @@ -404,31 +404,31 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return } - if (capabilities!!.getPublicPasswordEnforced() || capabilities!!.getAskForOptionalPassword()) { + if (capabilities!!.publicPasswordEnforced || capabilities!!.askForOptionalPassword) { // password enforced by server, request to the user before trying to create - requestPasswordForShareViaLink(true, capabilities!!.getAskForOptionalPassword()) + requestPasswordForShareViaLink(true, capabilities!!.askForOptionalPassword) } else { - executorService!!.submit(Runnable { + executorService!!.submit { val result: ApiResult?> = repository!!.addShare(note!!, ShareType.PUBLIC_LINK, "") - runOnUiThread(Runnable { - if (result is ApiResult.Success?> && binding!!.sharesList.getAdapter() is ShareeListAdapter) { + runOnUiThread { + if (result is ApiResult.Success?> && binding!!.sharesList.adapter is ShareeListAdapter) { DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) note!!.setIsShared(true) repository!!.updateNote(note!!) - val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + val adapter = binding!!.sharesList.adapter as ShareeListAdapter adapter.addShare(result.data!!.ocs.data!!.toOCShare()) } else if (result is ApiResult.Error) { DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) } - }) - }) + } + } } } fun requestPasswordForShareViaLink(createShare: Boolean, askForPassword: Boolean) { val dialog = newInstance(note, createShare, askForPassword, this) - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT) + dialog.show(supportFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } // TODO: @@ -442,21 +442,21 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar intentToShareLink.setType("text/plain") intentToShareLink.putExtra( Intent.EXTRA_SUBJECT, - getString(R.string.note_share_activity_subject_shared_with_you, note!!.getTitle()) + getString(R.string.note_share_activity_subject_shared_with_you, note!!.title) ) - val packagesToExclude = arrayOf(this.getPackageName()) + val packagesToExclude = arrayOf(this.packageName) val chooserDialog: DialogFragment = newInstance(intentToShareLink, *packagesToExclude) - chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG) + chooserDialog.show(supportFragmentManager, FTAG_CHOOSER_DIALOG) } private fun createInternalLink(): String { - val baseUri = Uri.parse(account!!.getUrl()) - return baseUri.toString() + "/index.php/f/" + note!!.getRemoteId() + val baseUri = account!!.url.toUri() + return baseUri.toString() + "/index.php/f/" + note!!.remoteId } override fun copyLink(share: OCShare) { - if (!note!!.isShared()) { + if (!note!!.isShared) { return } @@ -476,13 +476,13 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar Snackbar.LENGTH_LONG ) .setAction( - R.string.share, - View.OnClickListener { v: View? -> showShareLinkDialog(link) }) + R.string.share + ) { showShareLinkDialog(link) } snackbar.show() } override fun showSharingMenuActionSheet(share: OCShare?) { - if (!this.isFinishing()) { + if (!this.isFinishing) { NoteShareActivityShareItemActionBottomSheetDialog(this, this, share).show() } } @@ -494,7 +494,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar ) val snackbar = Snackbar .make(binding!!.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) - .setAction(R.string.dismiss, View.OnClickListener { view: View? -> }) + .setAction(R.string.dismiss) { } val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this) util.material.themeSnackbar(snackbar) snackbar.show() @@ -506,7 +506,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar override fun requestPasswordForShare(share: OCShare?, askForPassword: Boolean) { val dialog = newInstance(share, askForPassword, this) - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT) + dialog.show(supportFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } // TODO: @@ -525,27 +525,25 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar executorService!!.submit(Runnable submit@{ try { - val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter? + val adapter = binding!!.sharesList.adapter as ShareeListAdapter? if (adapter == null) { - runOnUiThread(Runnable { + runOnUiThread { DisplayUtils.showSnackMessage( this@NoteShareActivity, getString(R.string.could_not_retrieve_shares) ) - }) + } isInitializingShares = false return@submit } - - val remoteNotes: List - if (note != null) { - remoteNotes = repository!!.fetchSharesFromNote(note!!) + val remoteNotes: List = if (note != null) { + repository!!.fetchSharesFromNote(note!!) } else { - remoteNotes = listOf() + listOf() } - runOnUiThread(Runnable { + runOnUiThread { shares.clear() shares.addAll(remoteNotes) @@ -554,17 +552,17 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar addPublicShares(adapter) setShareWithYou() isInitializingShares = false - }) + } } catch (e: Exception) { - Log_OC.d(TAG, "Exception while updateShareeListAdapter: " + e) + Log_OC.d(TAG, "Exception while updateShareeListAdapter: $e") } }) } private fun addPublicShares(adapter: ShareeListAdapter) { - val publicShares: MutableList = ArrayList() + val publicShares: MutableList = ArrayList() - if (containsNoNewPublicShare(adapter.getShares())) { + if (containsNoNewPublicShare(adapter.shares)) { val ocShare = OCShare() ocShare.shareType = ShareType.NEW_PUBLIC_LINK publicShares.add(ocShare) @@ -591,7 +589,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) // FIXME: - if (intent.resolveActivity(getPackageManager()) != null) { + if (intent.resolveActivity(packageManager) != null) { onContactSelectionResultLauncher.launch(intent) } else { DisplayUtils.showSnackMessage( @@ -605,7 +603,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar // Define the projection to get all email addresses. val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS) - val cursor = getContentResolver().query(contactUri, projection, null, null, null) + val cursor = contentResolver.query(contactUri, projection, null, null, null) if (cursor == null) { DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) Log_OC.e(TAG, "Failed to pick email address as Cursor is null.") @@ -629,10 +627,10 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar // Use the email address as needed. // email variable contains the selected contact's email address. val email = cursor.getString(columnIndex) - binding!!.searchView.post(Runnable { + binding!!.searchView.post { binding!!.searchView.setQuery(email, false) binding!!.searchView.requestFocus() - }) + } cursor.close() } @@ -655,7 +653,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private fun isReshareForbidden(share: OCShare): Boolean { return (share.shareType != null && ShareType.FEDERATED == share.shareType) || - capabilities != null && !capabilities!!.isReSharingAllowed() + capabilities != null && !capabilities!!.isReSharingAllowed } @VisibleForTesting @@ -678,7 +676,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val result = repository!!.removeShare(share, note!!) runOnUiThread(Runnable { if (result) { - val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter? + val adapter = binding!!.sharesList.adapter as ShareeListAdapter? if (adapter == null) { DisplayUtils.showSnackMessage( this@NoteShareActivity, @@ -727,31 +725,31 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return false } - val majorVersionAsString = capabilities!!.getNextcloudMajorVersion() + val majorVersionAsString = capabilities!!.nextcloudMajorVersion if (majorVersionAsString != null) { val majorVersion = majorVersionAsString.toInt() return majorVersion >= 18 } return false - } catch (e: NumberFormatException) { + } catch (_: NumberFormatException) { Log_OC.d(TAG, "Exception while getting expDateShown") return false } } override fun onQuickPermissionChanged(share: OCShare, permission: Int) { - executorService!!.submit(Runnable { + executorService!!.submit { val result: ApiResult?> = repository!!.updateSharePermission(share.id, permission) - runOnUiThread(Runnable { - if (result.isSuccess?>()) { + runOnUiThread { + if (result.isSuccess()) { updateShare(share) } else if (result is ApiResult.Error) { DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) } - }) - }) + } + } } private fun updateShare(share: OCShare) { @@ -765,11 +763,11 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val updatedShares: List = repository!!.fetchSharesFromNote(note!!) runOnUiThread(Runnable { - if (binding!!.sharesList.getAdapter() is ShareeListAdapter) { + if (binding!!.sharesList.adapter is ShareeListAdapter) { var updatedShare: OCShare? = null for (i in updatedShares.indices) { - if (updatedShares.get(i)!!.id == share.id) { - updatedShare = updatedShares.get(i) + if (updatedShares[i]!!.id == share.id) { + updatedShare = updatedShares[i] break } } @@ -778,36 +776,37 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return@Runnable } - val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + val adapter = binding!!.sharesList.adapter as ShareeListAdapter adapter.updateShare(updatedShare) } }) } catch (e: Exception) { - Log_OC.d(TAG, "Exception while refreshing share info: " + e) + Log_OC.d(TAG, "Exception while refreshing share info: $e") } }) } private val requestContactPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission(), ActivityResultCallback { isGranted: Boolean? -> - if (isGranted == true) { - pickContactEmail() - } else { - BrandedSnackbar.make( - binding!!.getRoot(), - getString(R.string.note_share_activity_contact_no_permission), - Snackbar.LENGTH_LONG - ) - .show() - } - }) + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean? -> + if (isGranted == true) { + pickContactEmail() + } else { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.note_share_activity_contact_no_permission), + Snackbar.LENGTH_LONG + ) + .show() + } + } private val onContactSelectionResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult(), ActivityResultCallback { result: ActivityResult? -> - if (result!!.getResultCode() == RESULT_OK) { - val intent = result.getData() + if (result!!.resultCode == RESULT_OK) { + val intent = result.data if (intent == null) { BrandedSnackbar.make( binding!!.getRoot(), @@ -818,7 +817,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return@ActivityResultCallback } - val contactUri = intent.getData() + val contactUri = intent.data if (contactUri == null) { BrandedSnackbar.make( binding!!.getRoot(), @@ -842,7 +841,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar util.platform.colorImageView(binding!!.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT) util.platform.colorCircularProgressBar(binding!!.loadingLayoutIndicator, ColorRole.PRIMARY) util.platform.themeHorizontalProgressBar(binding!!.progressBar) - util.platform.colorViewBackground(getWindow().getDecorView()) + util.platform.colorViewBackground(window.decorView) util.platform.colorViewBackground(binding!!.getRoot()) util.material.colorMaterialButtonPrimaryOutlined(binding!!.btnShareButton) util.platform.colorTextView(binding!!.title, ColorRole.ON_SURFACE) @@ -861,26 +860,26 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return } - executorService!!.submit(Runnable { + executorService!!.submit { val result: ApiResult?> = repository!!.addShare( note, ShareType.PUBLIC_LINK, "", "false", password, - repository!!.getCapabilities().getDefaultPermission() + repository!!.getCapabilities().defaultPermission ) - runOnUiThread(Runnable { + runOnUiThread { if (result is ApiResult.Success?> && - binding!!.sharesList.getAdapter() is ShareeListAdapter + binding!!.sharesList.adapter is ShareeListAdapter ) { - val adapter = binding!!.sharesList.getAdapter() as ShareeListAdapter + val adapter = binding!!.sharesList.adapter as ShareeListAdapter adapter.addShare(result.data!!.ocs.data!!.toOCShare()) } else if (result is ApiResult.Error) { DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) } - }) - }) + } + } } override fun setPasswordToShare(share: OCShare, password: String?) { @@ -889,7 +888,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return } - executorService!!.submit(Runnable { + executorService!!.submit { run { val isDownloadAndAllowsSyncEnabled = repository!!.isAllowDownloadAndSync(share) val attributes = createAttributes( @@ -909,18 +908,18 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar ) val result: ApiResult?> = repository!!.updateShare(share.id, requestBody) - runOnUiThread(Runnable { - if (result.isSuccess?>()) { + runOnUiThread { + if (result.isSuccess()) { val intent = Intent(this@NoteShareActivity, MainActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) this@NoteShareActivity.startActivity(intent) - } else if (result.isError?>()) { + } else if (result.isError()) { val error = result as ApiResult.Error DisplayUtils.showSnackMessage(this@NoteShareActivity, error.message) } - }) + } } - }) + } } companion object { From c9d7e254184f47158e5828bf6f43750d5051a32d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:22:27 +0200 Subject: [PATCH 09/96] Converted NoteShareActivity to fragment Signed-off-by: daniele-verducci --- ...eShareActivity.kt => NoteShareFragment.kt} | 176 +++++++----------- .../shared/util/clipboard/ClipboardUtil.kt | 15 +- .../main/res/layout/activity_note_share.xml | 37 ---- 3 files changed, 75 insertions(+), 153 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/share/{NoteShareActivity.kt => NoteShareFragment.kt} (86%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt similarity index 86% rename from app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt rename to app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 146ada1b7..e4727264f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -1,15 +1,12 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ package it.niedermann.owncloud.notes.share import android.Manifest import android.app.Activity +import android.app.Activity.RESULT_OK import android.app.SearchManager import android.content.ComponentName +import android.content.Context.INPUT_METHOD_SERVICE +import android.content.Context.SEARCH_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -17,6 +14,7 @@ import android.os.Bundle import android.provider.ContactsContract import android.text.InputType import android.text.TextUtils +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo @@ -29,6 +27,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.core.view.isVisible +import androidx.core.view.size import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -38,7 +39,7 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.branding.BrandedActivity +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.ActivityNoteShareBinding @@ -82,11 +83,9 @@ import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -import androidx.core.view.size -import androidx.core.view.isVisible -import androidx.core.net.toUri +import kotlin.text.toInt -class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShareItemAction, +class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShareItemAction, QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { private var executorService: ScheduledExecutorService? = null private var future: Future<*>? = null @@ -99,46 +98,15 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private val shares: MutableList = Collections.synchronizedList(ArrayList()) - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { executorService = Executors.newSingleThreadScheduledExecutor() binding = ActivityNoteShareBinding.inflate(layoutInflater) - setContentView(binding!!.getRoot()) - setSupportActionBar(binding!!.toolbar) - binding!!.toolbar.setNavigationOnClickListener { backPressed() } - registerResultLauncher() initializeArguments() - initializeOnBackPressedDispatcher() - } - - private fun registerResultLauncher() { - resultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult? -> - if (result!!.resultCode == RESULT_OK && result.data != null) { - recreate() - } - } - } - - private fun initializeOnBackPressedDispatcher() { - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - backPressed() - } - }) - } - - private fun backPressed() { - val intent = Intent(this@NoteShareActivity, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - startActivity(intent) - finish() + return binding!!.getRoot() } private fun initializeArguments() { - val bundler = intent.extras + val bundler = arguments note = bundler.getSerializableArgument(ARG_NOTE, Note::class.java) account = bundler.getSerializableArgument(ARG_ACCOUNT, Account::class.java) @@ -149,28 +117,27 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar executorService!!.submit { try { val ssoAcc = - SingleAccountHelper.getCurrentSingleSignOnAccount(this@NoteShareActivity) - repository = ShareRepository(this@NoteShareActivity, ssoAcc) + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) + repository = ShareRepository(requireContext(), ssoAcc) capabilities = repository!!.getCapabilities() repository!!.fetchSharesForNotesAndSaveShareEntities() - runOnUiThread { - binding!!.fileName.text = note!!.title + activity?.runOnUiThread { binding!!.searchContainer.visibility = View.VISIBLE binding!!.sharesList.visibility = View.VISIBLE binding!!.sharesList.setAdapter( ShareeListAdapter( - this, + activity, ArrayList(), this, account ) ) - binding!!.sharesList.setLayoutManager(LinearLayoutManager(this)) + binding!!.sharesList.setLayoutManager(LinearLayoutManager(requireContext())) binding!!.pickContactEmailBtn.setOnClickListener { checkContactPermission() } binding!!.btnShareButton.setOnClickListener { ShareUtil.openShareDialog( - this, + requireContext(), note!!.title, note!!.content ) @@ -180,7 +147,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar setupReadOnlySearchView() } else { setupSearchView( - getSystemService(SEARCH_SERVICE) as SearchManager?, + context?.getSystemService(SEARCH_SERVICE) as SearchManager?, componentName ) } @@ -230,8 +197,8 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return } - val suggestionAdapter = SuggestionAdapter(this, null, account!!) - val provider = UsersAndGroupsSearchProvider(this, repository) + val suggestionAdapter = SuggestionAdapter(requireContext(), null, account!!) + val provider = UsersAndGroupsSearchProvider(requireContext(), repository) binding!!.searchView.setSuggestionsAdapter(suggestionAdapter) binding!!.searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) @@ -279,7 +246,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar return@schedule } - runOnUiThread { + activity?.runOnUiThread { if (binding!!.searchView.isVisible) { suggestionAdapter.swapCursor(cursor) } @@ -287,7 +254,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar } } catch (e: Exception) { Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: $e") - runOnUiThread { binding!!.progressBar.visibility = View.GONE } + activity?.runOnUiThread { binding!!.progressBar.visibility = View.GONE } } }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS) } else { @@ -300,7 +267,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar binding!!.searchView.setOnQueryTextFocusChangeListener { _: View?, hasFocus: Boolean -> if (hasFocus) { - val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + val imm = context?.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput( binding!!.searchView.findFocus(), InputMethodManager.SHOW_IMPLICIT @@ -338,7 +305,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar bundle.putString(NoteShareDetailActivity.ARG_SHAREE_NAME, shareWith) bundle.putInt(NoteShareDetailActivity.ARG_SHARE_TYPE, shareType) - val intent = Intent(this, NoteShareDetailActivity::class.java) + val intent = Intent(context, NoteShareDetailActivity::class.java) intent.putExtras(bundle) resultLauncher!!.launch(intent) } @@ -371,7 +338,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar getString(R.string.note_share_activity_shared_with_you), share.ownerDisplayName ) - AvatarLoader.load(this, binding!!.sharedWithYouAvatar, account!!) + AvatarLoader.load(requireContext(), binding!!.sharedWithYouAvatar, account!!) binding!!.sharedWithYouAvatar.setVisibility(View.VISIBLE) val description = share.note @@ -388,7 +355,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar override fun copyInternalLink() { if (account == null) { DisplayUtils.showSnackMessage( - this, + view, getString(R.string.note_share_activity_could_not_retrieve_url) ) return @@ -411,15 +378,15 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar executorService!!.submit { val result: ApiResult?> = repository!!.addShare(note!!, ShareType.PUBLIC_LINK, "") - runOnUiThread { + activity?.runOnUiThread { if (result is ApiResult.Success?> && binding!!.sharesList.adapter is ShareeListAdapter) { - DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + DisplayUtils.showSnackMessage(view, result.message) note!!.setIsShared(true) repository!!.updateNote(note!!) val adapter = binding!!.sharesList.adapter as ShareeListAdapter adapter.addShare(result.data!!.ocs.data!!.toOCShare()) } else if (result is ApiResult.Error) { - DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + DisplayUtils.showSnackMessage(view, result.message) } } } @@ -428,7 +395,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar fun requestPasswordForShareViaLink(createShare: Boolean, askForPassword: Boolean) { val dialog = newInstance(note, createShare, askForPassword, this) - dialog.show(supportFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) + dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } // TODO: @@ -445,9 +412,9 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar getString(R.string.note_share_activity_subject_shared_with_you, note!!.title) ) - val packagesToExclude = arrayOf(this.packageName) + val packagesToExclude = arrayOf(context?.packageName) val chooserDialog: DialogFragment = newInstance(intentToShareLink, *packagesToExclude) - chooserDialog.show(supportFragmentManager, FTAG_CHOOSER_DIALOG) + chooserDialog.show(parentFragmentManager, FTAG_CHOOSER_DIALOG) } private fun createInternalLink(): String { @@ -463,15 +430,15 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar if (TextUtils.isEmpty(share.shareLink)) { copyAndShareFileLink(share.shareLink) } else { - copyToClipboard(this, share.shareLink) + copyToClipboard(requireContext(), share.shareLink) } } private fun copyAndShareFileLink(link: String?) { - copyToClipboard(this, link, false) + copyToClipboard(requireContext(), link, false) val snackbar = Snackbar .make( - this.findViewById(android.R.id.content)!!, + requireView(), R.string.clipboard_text_copied, Snackbar.LENGTH_LONG ) @@ -482,9 +449,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar } override fun showSharingMenuActionSheet(share: OCShare?) { - if (!this.isFinishing) { - NoteShareActivityShareItemActionBottomSheetDialog(this, this, share).show() - } + NoteShareActivityShareItemActionBottomSheetDialog(activity, this, share).show() } override fun showShareExpirationSnackbar(share: OCShare) { @@ -495,18 +460,18 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val snackbar = Snackbar .make(binding!!.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) .setAction(R.string.dismiss) { } - val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this) + val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(requireContext()), requireContext()) util.material.themeSnackbar(snackbar) snackbar.show() } override fun showPermissionsDialog(share: OCShare?) { - QuickSharingPermissionsBottomSheetDialog(this, this, share).show() + QuickSharingPermissionsBottomSheetDialog(activity, this, share).show() } override fun requestPasswordForShare(share: OCShare?, askForPassword: Boolean) { val dialog = newInstance(share, askForPassword, this) - dialog.show(supportFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) + dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } // TODO: @@ -528,9 +493,9 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val adapter = binding!!.sharesList.adapter as ShareeListAdapter? if (adapter == null) { - runOnUiThread { + activity?.runOnUiThread { DisplayUtils.showSnackMessage( - this@NoteShareActivity, + view, getString(R.string.could_not_retrieve_shares) ) } @@ -543,7 +508,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar listOf() } - runOnUiThread { + activity?.runOnUiThread { shares.clear() shares.addAll(remoteNotes) @@ -575,7 +540,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private fun checkContactPermission() { if (ContextCompat.checkSelfPermission( - this, + requireContext(), Manifest.permission.READ_CONTACTS ) == PackageManager.PERMISSION_GRANTED ) { @@ -589,11 +554,11 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) // FIXME: - if (intent.resolveActivity(packageManager) != null) { + if (intent.resolveActivity(requireContext().packageManager) != null) { onContactSelectionResultLauncher.launch(intent) } else { DisplayUtils.showSnackMessage( - this, + view, getString(R.string.file_detail_sharing_fragment_no_contact_app_message) ) } @@ -603,15 +568,15 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar // Define the projection to get all email addresses. val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS) - val cursor = contentResolver.query(contactUri, projection, null, null, null) + val cursor = context?.contentResolver?.query(contactUri, projection, null, null, null) if (cursor == null) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) Log_OC.e(TAG, "Failed to pick email address as Cursor is null.") return } if (!cursor.moveToFirst()) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) Log_OC.e(TAG, "Failed to pick email address as no Email found.") return } @@ -619,7 +584,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar // The contact has only one email address, use it. val columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) if (columnIndex == -1) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)) + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) Log_OC.e(TAG, "Failed to pick email address.") return } @@ -653,13 +618,13 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar private fun isReshareForbidden(share: OCShare): Boolean { return (share.shareType != null && ShareType.FEDERATED == share.shareType) || - capabilities != null && !capabilities!!.isReSharingAllowed + capabilities != null && !capabilities!!.isReSharingAllowed } @VisibleForTesting fun search(query: String?) { - val searchView = findViewById(R.id.searchView) - searchView.setQuery(query, true) + val searchView = binding?.searchView + searchView?.setQuery(query, true) } override fun advancedPermissions(share: OCShare) { @@ -674,12 +639,12 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar override fun unShare(share: OCShare) { executorService!!.submit(Runnable { val result = repository!!.removeShare(share, note!!) - runOnUiThread(Runnable { + activity?.runOnUiThread(Runnable { if (result) { val adapter = binding!!.sharesList.adapter as ShareeListAdapter? if (adapter == null) { DisplayUtils.showSnackMessage( - this@NoteShareActivity, + view, getString(R.string.email_pick_failed) ) return@Runnable @@ -687,7 +652,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar adapter.remove(share) } else { DisplayUtils.showSnackMessage( - this@NoteShareActivity, + view, getString(R.string.failed_the_remove_share) ) } @@ -712,7 +677,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, this.expDateShown) bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail) - val intent = Intent(this, NoteShareDetailActivity::class.java) + val intent = Intent(context, NoteShareDetailActivity::class.java) intent.putExtras(bundle) resultLauncher!!.launch(intent) } @@ -742,11 +707,11 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar executorService!!.submit { val result: ApiResult?> = repository!!.updateSharePermission(share.id, permission) - runOnUiThread { + activity?.runOnUiThread { if (result.isSuccess()) { updateShare(share) } else if (result is ApiResult.Error) { - DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + DisplayUtils.showSnackMessage(view, result.message) } } } @@ -762,7 +727,7 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar try { val updatedShares: List = repository!!.fetchSharesFromNote(note!!) - runOnUiThread(Runnable { + activity?.runOnUiThread(Runnable { if (binding!!.sharesList.adapter is ShareeListAdapter) { var updatedShare: OCShare? = null for (i in updatedShares.indices) { @@ -833,19 +798,14 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar }) override fun applyBrand(color: Int) { - val util = BrandingUtil.of(color, this) - util.platform.themeStatusBar(this) - util.material.themeToolbar(binding!!.toolbar) + val util = BrandingUtil.of(color, requireContext()) util.notes.themeContentSearchView(binding!!.searchView) util.platform.colorImageView(binding!!.searchViewIcon, ColorRole.ON_SURFACE_VARIANT) util.platform.colorImageView(binding!!.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT) util.platform.colorCircularProgressBar(binding!!.loadingLayoutIndicator, ColorRole.PRIMARY) util.platform.themeHorizontalProgressBar(binding!!.progressBar) - util.platform.colorViewBackground(window.decorView) util.platform.colorViewBackground(binding!!.getRoot()) util.material.colorMaterialButtonPrimaryOutlined(binding!!.btnShareButton) - util.platform.colorTextView(binding!!.title, ColorRole.ON_SURFACE) - util.platform.colorTextView(binding!!.fileName, ColorRole.ON_SURFACE_VARIANT) util.notes.themeSearchCardView(binding!!.searchCardWrapper) } @@ -869,14 +829,14 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar password, repository!!.getCapabilities().defaultPermission ) - runOnUiThread { + activity?.runOnUiThread { if (result is ApiResult.Success?> && binding!!.sharesList.adapter is ShareeListAdapter ) { val adapter = binding!!.sharesList.adapter as ShareeListAdapter adapter.addShare(result.data!!.ocs.data!!.toOCShare()) } else if (result is ApiResult.Error) { - DisplayUtils.showSnackMessage(this@NoteShareActivity, result.message) + DisplayUtils.showSnackMessage(view, result.message) } } } @@ -908,14 +868,14 @@ class NoteShareActivity : BrandedActivity(), ShareeListAdapterListener, NoteShar ) val result: ApiResult?> = repository!!.updateShare(share.id, requestBody) - runOnUiThread { + activity?.runOnUiThread { if (result.isSuccess()) { - val intent = Intent(this@NoteShareActivity, MainActivity::class.java) + val intent = Intent(context, MainActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - this@NoteShareActivity.startActivity(intent) + activity?.startActivity(intent) } else if (result.isError()) { val error = result as ApiResult.Error - DisplayUtils.showSnackMessage(this@NoteShareActivity, error.message) + DisplayUtils.showSnackMessage(view, error.message) } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt index 0e46f8210..a4ef95a3e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt @@ -6,7 +6,6 @@ */ package it.niedermann.owncloud.notes.shared.util.clipboard -import android.app.Activity import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -24,26 +23,26 @@ object ClipboardUtil { @JvmStatic @JvmOverloads @Suppress("TooGenericExceptionCaught") - fun copyToClipboard(activity: Activity, text: String?, showToast: Boolean = true) { + fun copyToClipboard(context: Context, text: String?, showToast: Boolean = true) { if (!TextUtils.isEmpty(text)) { try { val clip = ClipData.newPlainText( - activity.getString( + context.getString( R.string.clipboard_label, - activity.getString(R.string.app_name) + context.getString(R.string.app_name) ), text ) - (activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip) + (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip) if (showToast) { - Toast.makeText(activity, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - Toast.makeText(activity, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show() Log_OC.e(TAG, "Exception caught while copying to clipboard", e) } } else { - Toast.makeText(activity, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/res/layout/activity_note_share.xml b/app/src/main/res/layout/activity_note_share.xml index ffeb8cf6b..121dab920 100644 --- a/app/src/main/res/layout/activity_note_share.xml +++ b/app/src/main/res/layout/activity_note_share.xml @@ -41,43 +41,6 @@ - - - - - - - - - - - - Date: Mon, 12 Oct 2026 18:23:14 +0200 Subject: [PATCH 10/96] Removed stale code Signed-off-by: daniele-verducci --- .../owncloud/notes/share/NoteShareFragment.kt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index e4727264f..2aba8c581 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -889,16 +889,5 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" private const val SEARCH_DELAY_MS: Long = 500 - - @JvmStatic - fun getActivityStartIntent(note: Note, account: Account, activity: Activity): Intent { - val bundle = Bundle() - bundle.putSerializable(ARG_NOTE, note) - bundle.putSerializable(ARG_ACCOUNT, account) - - val intent = Intent(activity, NoteShareActivity::class.java) - intent.putExtras(bundle) - return intent - } } } From d861262ea9a67f9fc65da1ee2020a3091ed7be8d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:34:31 +0200 Subject: [PATCH 11/96] Cleanup: the searchable wasn't present in the activity declaration in the manifest (removed in commit 409e1303b43e39622cae73697323668cfbef856b), so componentName is always null Signed-off-by: daniele-verducci --- .../owncloud/notes/share/NoteShareFragment.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 2aba8c581..914076074 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -4,9 +4,7 @@ import android.Manifest import android.app.Activity import android.app.Activity.RESULT_OK import android.app.SearchManager -import android.content.ComponentName import android.content.Context.INPUT_METHOD_SERVICE -import android.content.Context.SEARCH_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -146,10 +144,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar if (note!!.readonly) { setupReadOnlySearchView() } else { - setupSearchView( - context?.getSystemService(SEARCH_SERVICE) as SearchManager?, - componentName - ) + setupSearchView() } updateShareeListAdapter() @@ -191,17 +186,11 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar disableSearchView(binding!!.searchView) } - private fun setupSearchView(searchManager: SearchManager?, componentName: ComponentName?) { - if (searchManager == null) { - binding!!.searchView.visibility = View.GONE - return - } - + private fun setupSearchView() { val suggestionAdapter = SuggestionAdapter(requireContext(), null, account!!) val provider = UsersAndGroupsSearchProvider(requireContext(), repository) binding!!.searchView.setSuggestionsAdapter(suggestionAdapter) - binding!!.searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) binding!!.searchView.setIconifiedByDefault(false) binding!!.searchView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI binding!!.searchView.setQueryHint(resources.getString(R.string.note_share_activity_search_text)) From 46d3fa49a62e710cc4788833d315a71882a278ab Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:57:15 +0200 Subject: [PATCH 12/96] WIP passing required parameters Signed-off-by: daniele-verducci --- .../owncloud/notes/edit/BaseNoteFragment.java | 5 ++- .../notes/itemdetails/ItemDetailsActivity.kt | 33 ++++++++++++++++--- .../owncloud/notes/main/MainActivity.java | 1 - .../main/MultiSelectedActionModeCallback.java | 4 +-- .../owncloud/notes/share/NoteShareFragment.kt | 19 +++++++---- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index d41d13675..201ded2ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -307,10 +307,9 @@ public boolean onOptionsItemSelected(MenuItem item) { .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; } else if (itemId == R.id.menu_details) { - Intent detailsIntent = new Intent(requireContext(), ItemDetailsActivity.class); + Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(requireContext(), note, localAccount); startActivity(detailsIntent); - //shareNote(); // TODO - return false; + return true; } else if (itemId == MENU_ID_PIN) { pinNoteToHome(); return true; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index c310775c8..17cb8cc2c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -1,5 +1,7 @@ package it.niedermann.owncloud.notes.itemdetails +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator @@ -8,7 +10,10 @@ import it.niedermann.owncloud.notes.activities.ActivitiesFragment import it.niedermann.owncloud.notes.branding.BrandedActivity import it.niedermann.owncloud.notes.branding.BrandedFragment import it.niedermann.owncloud.notes.databinding.ActivityItemDetailsBinding -import it.niedermann.owncloud.notes.share.NoteShareActivity +import it.niedermann.owncloud.notes.persistence.entity.Account +import it.niedermann.owncloud.notes.persistence.entity.Note +import it.niedermann.owncloud.notes.share.NoteShareFragment +import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument class ItemDetailsActivity : BrandedActivity() { @@ -19,10 +24,14 @@ class ItemDetailsActivity : BrandedActivity() { binding = ActivityItemDetailsBinding.inflate(layoutInflater) setContentView(binding.root) + val note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) + ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") + val account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) + ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") + val pages = listOf>( - Pair(getString(R.string.activities_title), ActivitiesFragment.newInstance()) - // TODO: convert NoteShareActivity to Kotlin and refactor to be a BrandedFragment - //Pair(getString(R.string.share), NoteShareFragment.newInstance()) + Pair(getString(R.string.activities_title), ActivitiesFragment.newInstance()), + Pair(getString(R.string.share), NoteShareFragment.newInstance(note, account)) ) binding.pager.adapter = PagerAdapter(pages) @@ -42,4 +51,20 @@ class ItemDetailsActivity : BrandedActivity() { override fun createFragment(position: Int) = fragments[position].second } + companion object { + const val ARG_NOTE: String = "note" + const val ARG_ACCOUNT: String = "account" + + @JvmStatic + fun getActivityStartIntent(context: Context, note: Note, account: Account): Intent { + val bundle = Bundle() + bundle.putSerializable(ARG_NOTE, note) + bundle.putSerializable(ARG_ACCOUNT, account) + + val intent = Intent(context, ItemDetailsActivity::class.java) + intent.putExtras(bundle) + return intent + } + } + } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 2f8fe3855..456612eff 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -115,7 +115,6 @@ import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; -import it.niedermann.owncloud.notes.share.NoteShareActivity; import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 463c66c7a..67ad54e75 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -34,8 +34,6 @@ import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; -import it.niedermann.owncloud.notes.share.NoteShareActivity; -import it.niedermann.owncloud.notes.shared.util.ShareUtil; public class MultiSelectedActionModeCallback implements Callback { @@ -188,7 +186,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { }); return true; } else if (itemId == R.id.menu_details) { - Intent detailsIntent = new Intent(context, ItemDetailsActivity.class); + Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(context, note, localAccount); mainActivity.startActivity(detailsIntent); } return false; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 914076074..4d7e4a6f4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -599,12 +599,6 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar return true } - public override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putSerializable(ARG_NOTE, note) - outState.putSerializable(ARG_ACCOUNT, account) - } - private fun isReshareForbidden(share: OCShare): Boolean { return (share.shareType != null && ShareType.FEDERATED == share.shareType) || capabilities != null && !capabilities!!.isReSharingAllowed @@ -872,11 +866,22 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar } companion object { - private const val TAG = "NoteShareActivity" + private const val TAG = "NoteShareFragment" const val ARG_NOTE: String = "NOTE" const val ARG_ACCOUNT: String = "ACCOUNT" const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" private const val SEARCH_DELAY_MS: Long = 500 + + fun newInstance(note: Note, account: Account) : NoteShareFragment { + val f = NoteShareFragment() + + val bundle = Bundle() + bundle.putSerializable(ARG_NOTE, note) + bundle.putSerializable(ARG_ACCOUNT, account) + + f.arguments = bundle + return f + } } } From 779337d204186c316f06dcd7d74559ff2f8377f3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:14:57 +0200 Subject: [PATCH 13/96] Wired new details screen in place of old share, preserved system share for multiple selection Signed-off-by: daniele-verducci --- .../owncloud/notes/main/MainActivity.java | 5 +- .../main/MultiSelectedActionModeCallback.java | 47 ++++++++++--------- .../notes/main/items/NoteViewHolder.java | 11 ++--- .../main/items/grid/NoteViewGridHolder.java | 2 +- .../grid/NoteViewGridHolderOnlyTitle.java | 2 +- .../main/items/list/NoteViewListHolder.java | 2 +- .../notes/shared/model/NoteClickListener.java | 2 +- .../layout/item_notes_list_note_item_grid.xml | 4 +- ...m_notes_list_note_item_grid_only_title.xml | 4 +- ...item_notes_list_note_item_with_excerpt.xml | 4 +- .../res/menu/menu_list_context_multiple.xml | 7 +++ app/src/main/res/values/strings.xml | 1 + 12 files changed, 49 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 456612eff..c25ddcf1f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -101,6 +101,7 @@ import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.main.items.ItemAdapter; import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper; @@ -915,10 +916,10 @@ public void onNoteFavoriteClick(int position, View view) { } @Override - public void openShare(int position) { + public void openDetails(int position) { if (adapter.getItem(position) instanceof Note note) { mainViewModel.getCurrentAccount().observe(this, (account) -> { - final Intent intent = NoteShareActivity.getActivityStartIntent(note, account, this); + final Intent intent = ItemDetailsActivity.getActivityStartIntent(this, note, account); startActivity(intent); }); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 67ad54e75..3c2d8e3b6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -8,7 +8,6 @@ import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; @@ -34,6 +33,7 @@ import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; +import it.niedermann.owncloud.notes.shared.util.ShareUtil; public class MultiSelectedActionModeCallback implements Callback { @@ -101,6 +101,8 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // Info menu makes sense only for single selection + menu.findItem(R.id.menu_details).setEnabled(tracker.getSelection().size() == 1); return false; } @@ -152,31 +154,17 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName())); }); return true; - } /*else if (itemId == R.id.menu_share) { + } else if (itemId == R.id.menu_share) { final var selection = new ArrayList(tracker.getSelection().size()); for (final var sel : tracker.getSelection()) { selection.add(sel); } tracker.clearSelection(); - - if (selection.size() == 1) { - final var currentAccount$ = mainViewModel.getCurrentAccount(); - currentAccount$.observe(lifecycleOwner, account -> { - currentAccount$.removeObservers(lifecycleOwner); - executor.submit(() -> {{ - final var note = mainViewModel.getFullNote(selection.get(0)); - final Intent intent = NoteShareActivity.getActivityStartIntent(note, account, mainActivity); - mainActivity.startActivity(intent); - }}); - }); - } else { - ShareUtil.openShareDialog(context, - context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), - mainViewModel.collectNoteContents(selection)); - } - + ShareUtil.openShareDialog(context, + context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), + mainViewModel.collectNoteContents(selection)); return true; - }*/ else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it + } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it final var accountLiveData = mainViewModel.getCurrentAccount(); accountLiveData.observe(lifecycleOwner, account -> { accountLiveData.removeObservers(lifecycleOwner); @@ -186,8 +174,23 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { }); return true; } else if (itemId == R.id.menu_details) { - Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(context, note, localAccount); - mainActivity.startActivity(detailsIntent); + final var selection = new ArrayList(tracker.getSelection().size()); + for (final var sel : tracker.getSelection()) { + selection.add(sel); + } + tracker.clearSelection(); + + if (selection.size() == 1) { + final var currentAccount$ = mainViewModel.getCurrentAccount(); + currentAccount$.observe(lifecycleOwner, account -> { + currentAccount$.removeObservers(lifecycleOwner); + executor.submit(() -> {{ + final var note = mainViewModel.getFullNote(selection.get(0)); + Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(context, note, account); + mainActivity.startActivity(detailsIntent); + }}); + }); + } } return false; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java index 706c5bd26..d68ac162a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java @@ -114,16 +114,11 @@ protected void bindSearchableContent(@NonNull Context context, @NonNull TextView } } - protected void bindNoteSharedIcon(@NonNull Context context, boolean shared, @NonNull MaterialButton noteShareIcon, int color) { - if (shared) { - noteShareIcon.setVisibility(VISIBLE); - noteShareIcon.setOnClickListener(view -> noteClickListener.openShare(getLayoutPosition())); - } else { - noteShareIcon.setVisibility(GONE); - } + protected void bindNoteDetailsIcon(@NonNull Context context, @NonNull MaterialButton noteDetailsIcon, int color) { + noteDetailsIcon.setOnClickListener(view -> noteClickListener.openDetails(getLayoutPosition())); final var util = BrandingUtil.of(color, context); - util.material.colorMaterialButtonContent(noteShareIcon, ColorRole.PRIMARY); + util.material.colorMaterialButtonContent(noteDetailsIcon, ColorRole.SECONDARY); } public abstract void showSwipe(float dX); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java index 67c5c53d9..49b6aef25 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java @@ -55,7 +55,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color); - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteShared, color); binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java index 0699dd237..9763e33d5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java @@ -46,7 +46,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, i bindFavorite(binding.noteFavorite, note.getFavorite()); bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteShared, color); } @Nullable diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java index 771db1737..bb3e9cdd8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java @@ -74,7 +74,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), color); } - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteShared, color); } @NonNull diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java index 0f1a4084f..5e24d0303 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java @@ -13,5 +13,5 @@ public interface NoteClickListener { void onNoteFavoriteClick(int position, View v); - void openShare(int position); + void openDetails(int position); } \ No newline at end of file diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml index 2191dba0c..6946d69c9 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml @@ -126,7 +126,7 @@ tools:text="27.11." /> + + Category Favorite Preview + Share Shared note Loading… From 9eea55fe53201ef4452d59e3758afd2b96ab4013 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:21:57 +0200 Subject: [PATCH 14/96] Moved collectNoteContents to worker thread to fix lint error (already present on main branch) Signed-off-by: daniele-verducci --- .../notes/main/MultiSelectedActionModeCallback.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 3c2d8e3b6..0e49e6970 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -160,9 +160,12 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { selection.add(sel); } tracker.clearSelection(); - ShareUtil.openShareDialog(context, - context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), - mainViewModel.collectNoteContents(selection)); + executor.submit(() -> { + final var noteContents = mainViewModel.collectNoteContents(selection); + mainActivity.runOnUiThread(() -> ShareUtil.openShareDialog(context, + context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), + noteContents)); + }); return true; } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it final var accountLiveData = mainViewModel.getCurrentAccount(); From 7edb00a482808f827c28b9f1b7e1c14ac1b0a4c6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:52:47 +0200 Subject: [PATCH 15/96] Fixed view name Signed-off-by: daniele-verducci --- .../owncloud/notes/main/items/grid/NoteViewGridHolder.java | 2 +- .../notes/main/items/grid/NoteViewGridHolderOnlyTitle.java | 2 +- .../owncloud/notes/main/items/list/NoteViewListHolder.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java index 49b6aef25..0f5505e74 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java @@ -55,7 +55,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color); - bindNoteDetailsIcon(context, binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java index 9763e33d5..597b86d22 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java @@ -46,7 +46,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, i bindFavorite(binding.noteFavorite, note.getFavorite()); bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); - bindNoteDetailsIcon(context, binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); } @Nullable diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java index bb3e9cdd8..f67ccb469 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java @@ -74,7 +74,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), color); } - bindNoteDetailsIcon(context, binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); } @NonNull From c847a2cdcf79e0bd7605ad48e8ba99fa0e6e04a4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 14:21:49 +0200 Subject: [PATCH 16/96] Header content, tabs icons, theming Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 5 +- .../notes/itemdetails/ItemDetailsActivity.kt | 68 ++++++++++++++++--- app/src/main/res/drawable/nav_activity.xml | 16 +++++ .../main/res/layout/activity_item_details.xml | 6 +- .../layout/item_notes_list_note_item_grid.xml | 4 +- ...m_notes_list_note_item_grid_only_title.xml | 4 +- ...item_notes_list_note_item_with_excerpt.xml | 4 +- 7 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 app/src/main/res/drawable/nav_activity.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index f95cb1448..73b39da8c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -4,7 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding class ActivitiesFragment : BrandedFragment() { @@ -17,7 +19,8 @@ class ActivitiesFragment : BrandedFragment() { } override fun applyBrand(color: Int) { - // TODO + val util = BrandingUtil.of(color, requireContext()) + util.platform.colorViewBackground(binding.getRoot()) } companion object { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 17cb8cc2c..fbb882068 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -2,13 +2,17 @@ package it.niedermann.owncloud.notes.itemdetails import android.content.Context import android.content.Intent +import android.graphics.drawable.Drawable import android.os.Bundle +import android.text.format.DateUtils +import androidx.appcompat.content.res.AppCompatResources import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.ActivitiesFragment import it.niedermann.owncloud.notes.branding.BrandedActivity import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityItemDetailsBinding import it.niedermann.owncloud.notes.persistence.entity.Account import it.niedermann.owncloud.notes.persistence.entity.Note @@ -18,37 +22,83 @@ import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgume class ItemDetailsActivity : BrandedActivity() { lateinit var binding : ActivityItemDetailsBinding + lateinit var note: Note + lateinit var account: Account override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + parseArguments() + binding = ActivityItemDetailsBinding.inflate(layoutInflater) setContentView(binding.root) - val note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) + loadHeaderData() + loadFragments() + } + + private fun parseArguments() { + note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") - val account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) + account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") + } - val pages = listOf>( - Pair(getString(R.string.activities_title), ActivitiesFragment.newInstance()), - Pair(getString(R.string.share), NoteShareFragment.newInstance(note, account)) + private fun loadHeaderData() { + binding.filename.text = note.title + binding.favorite.setImageResource( + if (note.favorite) R.drawable.ic_star_yellow_24dp + else R.drawable.ic_star_border_grey_ccc_24dp + ) + binding.size.text = "TODO" + binding.lastModificationTimestamp.text = note.modified?.let { + DateUtils.formatDateTime( + this, + it.timeInMillis, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME + ) + } + } + + private fun loadFragments() { + val pages = listOf( + Page( + getString(R.string.activities_title), + AppCompatResources.getDrawable(this, R.drawable.nav_activity), + ActivitiesFragment.newInstance() + ), + Page( + getString(R.string.share), + AppCompatResources.getDrawable(this, R.drawable.ic_share_white_24dp), + NoteShareFragment.newInstance(note, account) + ) ) binding.pager.adapter = PagerAdapter(pages) TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> - tab.text = pages[position].first + tab.text = pages[position].title + tab.icon = pages[position].icon }.attach() } override fun applyBrand(color: Int) { - // TODO + val util = BrandingUtil.of(color, this) + util.platform.colorViewBackground(binding.getRoot()) + util.platform.colorViewBackground(binding.tabLayout) + util.material.themeTabLayout(binding.tabLayout) } - private inner class PagerAdapter(val fragments: List>) : + private data class Page ( + val title: String, + val icon: Drawable?, + val fragment: BrandedFragment + ) + + private inner class PagerAdapter(val fragments: List) : FragmentStateAdapter(this@ItemDetailsActivity) { override fun getItemCount() = fragments.size - override fun createFragment(position: Int) = fragments[position].second + override fun createFragment(position: Int) = fragments[position].fragment } companion object { diff --git a/app/src/main/res/drawable/nav_activity.xml b/app/src/main/res/drawable/nav_activity.xml new file mode 100644 index 000000000..32809f5d7 --- /dev/null +++ b/app/src/main/res/drawable/nav_activity.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml index af3c40443..7569eafa2 100644 --- a/app/src/main/res/layout/activity_item_details.xml +++ b/app/src/main/res/layout/activity_item_details.xml @@ -103,7 +103,8 @@ + android:layout_height="match_parent" + android:layout_marginTop="@dimen/spacer_2x"/> - diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml index 6946d69c9..21425b0a6 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml @@ -132,14 +132,12 @@ android:layout_height="@dimen/note_shared_icon_size" android:layout_marginEnd="@dimen/spacer_2x" android:contentDescription="@string/menu_shared_note" - android:visibility="gone" android:layout_gravity="end" app:backgroundTint="@android:color/transparent" app:icon="@drawable/ic_dots_vertical" app:iconGravity="textStart" app:iconSize="@dimen/note_shared_icon_size" - app:iconTint="@color/defaultBrand" - tools:visibility="visible" /> + app:iconTint="@color/defaultBrand" /> diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml index af0fe08ea..23239131e 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml @@ -121,14 +121,12 @@ android:layout_height="@dimen/note_shared_icon_size" android:layout_marginEnd="@dimen/spacer_2x" android:contentDescription="@string/menu_shared_note" - android:visibility="gone" android:layout_gravity="end" app:backgroundTint="@android:color/transparent" app:icon="@drawable/ic_dots_vertical" app:iconGravity="textStart" app:iconSize="@dimen/note_shared_icon_size" - app:iconTint="@color/defaultBrand" - tools:visibility="visible" /> + app:iconTint="@color/defaultBrand" /> diff --git a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml index a88b08936..995a859eb 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml @@ -175,13 +175,11 @@ android:layout_height="@dimen/note_shared_icon_size" android:layout_marginStart="@dimen/spacer_1x" android:contentDescription="@string/menu_shared_note" - android:visibility="gone" app:backgroundTint="@android:color/transparent" app:icon="@drawable/ic_dots_vertical" app:iconGravity="textStart" app:iconSize="@dimen/note_shared_icon_size" - app:iconTint="@color/defaultBrand" - tools:visibility="visible" /> + app:iconTint="@color/defaultBrand" /> From 1c431d7e9cc92ee201b40391d90133d086eab955 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 14:40:45 +0200 Subject: [PATCH 17/96] Actionbar Signed-off-by: daniele-verducci --- .../owncloud/notes/itemdetails/ItemDetailsActivity.kt | 3 +++ app/src/main/res/layout/activity_item_details.xml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index fbb882068..95f8f14e3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -33,6 +33,8 @@ class ItemDetailsActivity : BrandedActivity() { binding = ActivityItemDetailsBinding.inflate(layoutInflater) setContentView(binding.root) + binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + loadHeaderData() loadFragments() } @@ -83,6 +85,7 @@ class ItemDetailsActivity : BrandedActivity() { override fun applyBrand(color: Int) { val util = BrandingUtil.of(color, this) + util.material.themeToolbar(binding.toolbar); util.platform.colorViewBackground(binding.getRoot()) util.platform.colorViewBackground(binding.tabLayout) util.material.themeTabLayout(binding.tabLayout) diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml index 7569eafa2..e5ec37e0f 100644 --- a/app/src/main/res/layout/activity_item_details.xml +++ b/app/src/main/res/layout/activity_item_details.xml @@ -12,6 +12,15 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + Date: Tue, 13 Oct 2026 14:46:22 +0200 Subject: [PATCH 18/96] Removed size (we don't have such info right now) Signed-off-by: daniele-verducci --- .../owncloud/notes/itemdetails/ItemDetailsActivity.kt | 1 - app/src/main/res/layout/activity_item_details.xml | 8 -------- app/src/main/res/values/strings.xml | 1 - 3 files changed, 10 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 95f8f14e3..903cb7b34 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -52,7 +52,6 @@ class ItemDetailsActivity : BrandedActivity() { if (note.favorite) R.drawable.ic_star_yellow_24dp else R.drawable.ic_star_border_grey_ccc_24dp ) - binding.size.text = "TODO" binding.lastModificationTimestamp.text = note.modified?.let { DateUtils.formatDateTime( this, diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml index e5ec37e0f..c4bee62d6 100644 --- a/app/src/main/res/layout/activity_item_details.xml +++ b/app/src/main/res/layout/activity_item_details.xml @@ -60,14 +60,6 @@ android:contentDescription="@string/activities_favorite" android:src="@drawable/ic_star_border_grey_ccc_24dp" /> - - Note.md Favorite - 389 KB  • 12:23:45 From 843137066add02c4b12f739b783450a7a988a580 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 17:39:41 +0200 Subject: [PATCH 19/96] Stubbed activities API and repository Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 62 ++++++++++++++++++- .../repository/ActivitiesRepository.kt | 33 ++++++++++ .../notes/itemdetails/ItemDetailsActivity.kt | 2 +- .../notes/persistence/ApiProvider.java | 11 ++++ .../notes/persistence/sync/ActivitiesAPI.kt | 32 ++++++++++ 5 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 73b39da8c..d9e70b787 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -1,31 +1,87 @@ 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 com.nextcloud.android.common.ui.theme.utils.ColorRole +import androidx.lifecycle.lifecycleScope +import com.nextcloud.android.sso.helper.SingleAccountHelper +import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository 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.share.NoteShareFragment.Companion.ARG_ACCOUNT +import it.niedermann.owncloud.notes.share.NoteShareFragment.Companion.ARG_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 class ActivitiesFragment : BrandedFragment() { lateinit var binding : FragmentActivitiesBinding + private lateinit var note: Note + private lateinit var account: Account + + private var executorService: ScheduledExecutorService? = null + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + executorService = Executors.newSingleThreadScheduledExecutor() binding = FragmentActivitiesBinding.inflate(inflater) + + initializeArguments() + fetchActivities() + return super.onCreateView(inflater, container, savedInstanceState) } + 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") + } + override fun applyBrand(color: Int) { val util = BrandingUtil.of(color, requireContext()) util.platform.colorViewBackground(binding.getRoot()) } + private fun fetchActivities() { + executorService!!.submit(Runnable submit@{ + val ssoAccount = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) + val repository = ActivitiesRepository(requireContext(), ssoAccount) + lifecycleScope.launch(Dispatchers.IO) { + val activities = repository.fetchActivities(note) + withContext(Dispatchers.Main) { + Log.d("DEBUG", activities.toString()) + } + } + + }) + } + companion object { - fun newInstance() : ActivitiesFragment { - return ActivitiesFragment() + const val ARG_NOTE: String = "NOTE" + const val ARG_ACCOUNT: String = "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 } } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt new file mode 100644 index 000000000..456f776d4 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -0,0 +1,33 @@ +package it.niedermann.owncloud.notes.activities.repository + +import android.content.Context +import android.util.Log +import com.nextcloud.android.sso.model.SingleSignOnAccount +import it.niedermann.owncloud.notes.persistence.ApiProvider +import it.niedermann.owncloud.notes.persistence.entity.Note + +class ActivitiesRepository( + private val context: Context, + private val ssoAccount: SingleSignOnAccount +) { + + private val api by lazy { ApiProvider.getInstance().getActivitiesAPI(context, ssoAccount) } + + suspend fun fetchActivities(note: Note) { + val lastGiven = 0L // TODO what's this? + val response = api.fetch(note.id, lastGiven) + if(response.isSuccess) { + Log.d("DEBUG", response.resultData.toString()) + return + } + Log.e("DEBUG", "Unable to fetch activities") + + } + + + + + companion object { + private const val TAG = "ActivitiesRepository" + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 903cb7b34..c6655df24 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -66,7 +66,7 @@ class ItemDetailsActivity : BrandedActivity() { Page( getString(R.string.activities_title), AppCompatResources.getDrawable(this, R.drawable.nav_activity), - ActivitiesFragment.newInstance() + ActivitiesFragment.newInstance(note, account) ), Page( getString(R.string.share), diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 8143a9a9e..77315b164 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import it.niedermann.owncloud.notes.persistence.sync.ActivitiesAPI; import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer; import it.niedermann.owncloud.notes.persistence.sync.FilesAPI; import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; @@ -60,6 +61,7 @@ public class ApiProvider { private static final Map API_CACHE_NOTES = new ConcurrentHashMap<>(); private static final Map API_CACHE_FILES = new ConcurrentHashMap<>(); private static final Map API_CACHE_FILES_SHARING = new ConcurrentHashMap<>(); + private static final Map API_CACHE_ACTIVITIES = new ConcurrentHashMap<>(); public static ApiProvider getInstance() { @@ -145,6 +147,15 @@ private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @Non } } + public synchronized ActivitiesAPI getActivitiesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { + if (API_CACHE_ACTIVITIES.containsKey(ssoAccount.name)) { + return API_CACHE_ACTIVITIES.get(ssoAccount.name); + } + final var api = new ActivitiesAPI(context, ssoAccount); + API_CACHE_ACTIVITIES.put(ssoAccount.name, api); + return api; + } + /** * Invalidates the API cache for the given {@param ssoAccount} * diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt new file mode 100644 index 000000000..de8a30634 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt @@ -0,0 +1,32 @@ +package it.niedermann.owncloud.notes.persistence.sync + +import android.content.Context +import android.net.Uri +import com.nextcloud.android.sso.model.SingleSignOnAccount +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.activities.GetActivitiesRemoteOperation + +class ActivitiesAPI ( + val context: Context, + val ssoAccount: SingleSignOnAccount +) { + + val client = NextcloudClient( + Uri.parse(ssoAccount.url), + ssoAccount.userId, + "???", // TODO what's this? + context + + ) + + suspend fun fetch(noteId: Long, lastGiven: Long) : RemoteOperationResult { + val op = GetActivitiesRemoteOperation( + noteId, + lastGiven + ) + return op.execute(client) as RemoteOperationResult + } + + +} \ No newline at end of file From a7c9b43b86e84b1070792faf82244520b4784088 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 12:21:29 +0200 Subject: [PATCH 20/96] Missing copyright headers Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 6 ++++++ .../owncloud/notes/itemdetails/ItemDetailsActivity.kt | 7 +++++++ .../owncloud/notes/persistence/sync/ActivitiesAPI.kt | 6 ++++++ .../niedermann/owncloud/notes/share/NoteShareFragment.kt | 7 +++++++ app/src/main/res/drawable/ic_activity.xml | 2 +- app/src/main/res/drawable/nav_activity.xml | 2 +- app/src/main/res/layout/activity_item_details.xml | 1 + app/src/main/res/layout/fragment_activities.xml | 8 +++++++- 8 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index d9e70b787..f13046301 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -22,6 +22,12 @@ import kotlinx.coroutines.withContext import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ class ActivitiesFragment : BrandedFragment() { lateinit var binding : FragmentActivitiesBinding diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index c6655df24..e37fe4a04 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -19,6 +19,13 @@ import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.share.NoteShareFragment import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument + +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ class ItemDetailsActivity : BrandedActivity() { lateinit var binding : ActivityItemDetailsBinding diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt index de8a30634..3cdb457b0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.persistence.sync import android.content.Context diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 4d7e4a6f4..38a41f395 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -83,6 +83,13 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import kotlin.text.toInt + +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShareItemAction, QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { private var executorService: ScheduledExecutorService? = null diff --git a/app/src/main/res/drawable/ic_activity.xml b/app/src/main/res/drawable/ic_activity.xml index a9505658c..010b857e8 100644 --- a/app/src/main/res/drawable/ic_activity.xml +++ b/app/src/main/res/drawable/ic_activity.xml @@ -1,5 +1,5 @@ diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml index 3578d96f5..509ea2ce7 100644 --- a/app/src/main/res/layout/fragment_activities.xml +++ b/app/src/main/res/layout/fragment_activities.xml @@ -1,5 +1,11 @@ - android:layout_width="match_parent" android:layout_height="match_parent"> From ab4c2cadcd3d0288347a79d69c64e7852a4a8e98 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 12:22:03 +0200 Subject: [PATCH 21/96] Activities list layout Signed-off-by: daniele-verducci --- .../main/res/layout/fragment_activities.xml | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml index 509ea2ce7..b38663af9 100644 --- a/app/src/main/res/layout/fragment_activities.xml +++ b/app/src/main/res/layout/fragment_activities.xml @@ -1,4 +1,3 @@ - + - + + + + + + + + + android:layout_gravity="center" + android:indeterminate="true" /> - \ No newline at end of file + From 9502ea770ea744074030a8ec61c23d6b5f16f49a Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 13:09:28 +0200 Subject: [PATCH 22/96] Implemented activities APIs Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 22 ++++++++-- .../notes/activities/model/Activity.kt | 23 ++++++++++ .../repository/ActivitiesRepository.kt | 32 +++++++------- .../activities/repository/ActivitiesResult.kt | 18 ++++++++ .../notes/persistence/ApiProvider.java | 5 ++- .../notes/persistence/sync/ActivitiesAPI.kt | 42 ++++++------------- app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index f13046301..b42a10965 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -8,13 +8,12 @@ import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.nextcloud.android.sso.helper.SingleAccountHelper import it.niedermann.owncloud.notes.activities.repository.ActivitiesRepository +import it.niedermann.owncloud.notes.activities.repository.ActivitiesResult 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.share.NoteShareFragment.Companion.ARG_ACCOUNT -import it.niedermann.owncloud.notes.share.NoteShareFragment.Companion.ARG_NOTE import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -61,14 +60,29 @@ class ActivitiesFragment : BrandedFragment() { } private fun fetchActivities() { + val noteId = note.remoteId ?: return + executorService!!.submit(Runnable submit@{ val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) val repository = ActivitiesRepository(requireContext(), ssoAccount) lifecycleScope.launch(Dispatchers.IO) { - val activities = repository.fetchActivities(note) + val result = repository.fetchActivities(noteId) withContext(Dispatchers.Main) { - Log.d("DEBUG", activities.toString()) + when { + result is ActivitiesResult.Success -> { + // TODO: Render list + result.activities.forEach { + Log.d("DEBUG", "Activity: ${it.type} ${it.message} ${it.datetime}") + } + } + result is ActivitiesResult.FailedNotModified -> { + // TODO: Show error + } + result is ActivitiesResult.Failed -> { + // TODO: Show error + } + } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt new file mode 100644 index 000000000..5b98c5c5e --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt @@ -0,0 +1,23 @@ +/* + * 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 + +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: String?, + @SerializedName("object_id") val objectId: Long?, + @SerializedName("object_name") val objectName: String? +) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 456f776d4..983a8dbca 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -1,33 +1,31 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context -import android.util.Log import com.nextcloud.android.sso.model.SingleSignOnAccount import it.niedermann.owncloud.notes.persistence.ApiProvider -import it.niedermann.owncloud.notes.persistence.entity.Note +import java.net.HttpURLConnection class ActivitiesRepository( private val context: Context, private val ssoAccount: SingleSignOnAccount -) { +) { private val api by lazy { ApiProvider.getInstance().getActivitiesAPI(context, ssoAccount) } - suspend fun fetchActivities(note: Note) { - val lastGiven = 0L // TODO what's this? - val response = api.fetch(note.id, lastGiven) - if(response.isSuccess) { - Log.d("DEBUG", response.resultData.toString()) - return - } - Log.e("DEBUG", "Unable to fetch activities") - - } - + fun fetchActivities(noteId: Long, since: Long? = null): ActivitiesResult { + val response = api.fetchActivities(noteId, since).execute() + if (response.isSuccessful) { + return ActivitiesResult.Success( + response.body()?.ocs?.data ?: listOf() + ) + } + if (response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return ActivitiesResult.FailedNotModified() + } - companion object { - private const val TAG = "ActivitiesRepository" + return ActivitiesResult.Failed() } -} \ No newline at end of file + +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt new file mode 100644 index 000000000..e6a77af6f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -0,0 +1,18 @@ +package it.niedermann.owncloud.notes.activities.repository + +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.activities.model.Activity + +sealed class ActivitiesResult(open val descriptionId: Int? = null) { + + data class Success(val activities: List) : ActivitiesResult() + + data class FailedNotModified( + override val descriptionId: Int = R.string.activities_fetch_failed_not_modified + ) : ActivitiesResult(descriptionId) + + data class Failed( + override val descriptionId: Int = R.string.activities_fetch_failed + ) : ActivitiesResult(descriptionId) + +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 77315b164..4880d934b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -53,6 +53,7 @@ public class ApiProvider { private static final String API_USER_STATUS = "/ocs/v2.php/apps/user_status/api/v1/"; private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/"; private static final String API_ENDPOINT_FILES_SHARING ="/ocs/v2.php/apps/files_sharing/api/v1/"; + private static final String API_ENDPOINT_ACTIVITY = "/ocs/v2.php/apps/activity/api/v2/"; private static final Map API_CACHE = new ConcurrentHashMap<>(); @@ -151,7 +152,7 @@ public synchronized ActivitiesAPI getActivitiesAPI(@NonNull Context context, @No if (API_CACHE_ACTIVITIES.containsKey(ssoAccount.name)) { return API_CACHE_ACTIVITIES.get(ssoAccount.name); } - final var api = new ActivitiesAPI(context, ssoAccount); + final var api = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_ACTIVITY).create(ActivitiesAPI.class); API_CACHE_ACTIVITIES.put(ssoAccount.name, api); return api; } @@ -174,6 +175,7 @@ public synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAcco API_CACHE_OCS.remove(ssoAccount.name); API_CACHE_USER_STATUS.remove(ssoAccount.name); API_CACHE_FILES_SHARING.remove(ssoAccount.name); + API_CACHE_ACTIVITIES.remove(ssoAccount.name); } /** @@ -194,5 +196,6 @@ public synchronized void invalidateAPICache() { API_CACHE_OCS.clear(); API_CACHE_USER_STATUS.clear(); API_CACHE_FILES_SHARING.clear(); + API_CACHE_ACTIVITIES.clear(); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt index 3cdb457b0..0bac4a3b4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt @@ -1,38 +1,22 @@ /* * Nextcloud Notes - Android Client * + * SPDX-FileCopyrightText: 2026 Daniele Verducci * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ package it.niedermann.owncloud.notes.persistence.sync -import android.content.Context -import android.net.Uri -import com.nextcloud.android.sso.model.SingleSignOnAccount -import com.nextcloud.common.NextcloudClient -import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.activities.GetActivitiesRemoteOperation +import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.shared.model.OcsResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query -class ActivitiesAPI ( - val context: Context, - val ssoAccount: SingleSignOnAccount -) { - - val client = NextcloudClient( - Uri.parse(ssoAccount.url), - ssoAccount.userId, - "???", // TODO what's this? - context - - ) - - suspend fun fetch(noteId: Long, lastGiven: Long) : RemoteOperationResult { - val op = GetActivitiesRemoteOperation( - noteId, - lastGiven - ) - return op.execute(client) as RemoteOperationResult - } - - -} \ No newline at end of file +interface ActivitiesAPI { + @GET("activity/filter?format=json&object_type=files&sort=desc") + fun fetchActivities( + @Query("object_id") fileId: Long, + @Query("since") since: Long? + ): Call>> +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95659f690..3d59fb592 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -548,7 +548,10 @@ Account not found! Details + Activities + Unable to fetch activities + No activity present for this note Note.md Favorite From 1a61699c7381cc08c20b98490fe4c69bb3951547 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 14:04:01 +0200 Subject: [PATCH 23/96] Implemented pagination for activities API Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 1 + .../activities/repository/ActivitiesRepository.kt | 10 ++++++++-- .../notes/activities/repository/ActivitiesResult.kt | 2 +- .../owncloud/notes/persistence/sync/ActivitiesAPI.kt | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index b42a10965..0e7454c8f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -75,6 +75,7 @@ class ActivitiesFragment : BrandedFragment() { result.activities.forEach { Log.d("DEBUG", "Activity: ${it.type} ${it.message} ${it.datetime}") } + Log.d("DEBUG", "Activities LastGiven is ${result.lastGiven}") } result is ActivitiesResult.FailedNotModified -> { // TODO: Show error diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 983a8dbca..68c3900da 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -13,11 +13,12 @@ class ActivitiesRepository( private val api by lazy { ApiProvider.getInstance().getActivitiesAPI(context, ssoAccount) } fun fetchActivities(noteId: Long, since: Long? = null): ActivitiesResult { - val response = api.fetchActivities(noteId, since).execute() + val response = api.fetchActivities(noteId, since, PAGE_SIZE).execute() if (response.isSuccessful) { return ActivitiesResult.Success( - response.body()?.ocs?.data ?: listOf() + response.body()?.ocs?.data ?: listOf(), + response.headers()[HEADER_LAST_GIVEN]?.toLongOrNull() ) } @@ -28,4 +29,9 @@ class ActivitiesRepository( return ActivitiesResult.Failed() } + companion object { + private const val PAGE_SIZE = 50 + private const val HEADER_LAST_GIVEN = "X-Activity-Last-Given" + } + } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt index e6a77af6f..d2cd6b282 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -5,7 +5,7 @@ import it.niedermann.owncloud.notes.activities.model.Activity sealed class ActivitiesResult(open val descriptionId: Int? = null) { - data class Success(val activities: List) : ActivitiesResult() + data class Success(val activities: List, val lastGiven: Long?) : ActivitiesResult() data class FailedNotModified( override val descriptionId: Int = R.string.activities_fetch_failed_not_modified diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt index 0bac4a3b4..15611bf20 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt @@ -17,6 +17,7 @@ interface ActivitiesAPI { @GET("activity/filter?format=json&object_type=files&sort=desc") fun fetchActivities( @Query("object_id") fileId: Long, - @Query("since") since: Long? + @Query("since") since: Long?, + @Query("limit") limit: Int ): Call>> } From 0aaf829e113522a9c62b9b2813d1751507912ba3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 16:45:11 +0200 Subject: [PATCH 24/96] Implemented activities list, fix activity optional fields Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 40 +++++++-- .../activities/adapter/ActivityListAdapter.kt | 86 +++++++++++++++++++ .../adapter/OnActivityClickListener.kt | 7 ++ .../notes/activities/model/Activity.kt | 11 +-- .../main/res/layout/activity_list_item.xml | 84 ++++++++++++++++++ app/src/main/res/values/dimens.xml | 7 ++ app/src/main/res/values/strings.xml | 3 + 7 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt create mode 100644 app/src/main/res/layout/activity_list_item.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 0e7454c8f..9a9f4fb4d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -7,6 +7,9 @@ import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.nextcloud.android.sso.helper.SingleAccountHelper +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.branding.BrandedFragment @@ -24,14 +27,16 @@ import java.util.concurrent.ScheduledExecutorService /* * Nextcloud Notes - Android Client * + * SPDX-FileCopyrightText: 2026 Daniele Verducci * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -class ActivitiesFragment : BrandedFragment() { +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 executorService: ScheduledExecutorService? = null @@ -41,6 +46,7 @@ class ActivitiesFragment : BrandedFragment() { binding = FragmentActivitiesBinding.inflate(inflater) initializeArguments() + initializeList() fetchActivities() return super.onCreateView(inflater, container, savedInstanceState) @@ -54,6 +60,14 @@ class ActivitiesFragment : BrandedFragment() { ?: throw IllegalArgumentException("Argument $ARG_ACCOUNT is mandatory") } + private fun initializeList() { + adapter = ActivityListAdapter( + requireContext(), + this + ) + binding.list.adapter = adapter + } + override fun applyBrand(color: Int) { val util = BrandingUtil.of(color, requireContext()) util.platform.colorViewBackground(binding.getRoot()) @@ -62,6 +76,8 @@ class ActivitiesFragment : BrandedFragment() { private fun fetchActivities() { val noteId = note.remoteId ?: return + binding.progress.visibility = View.VISIBLE + executorService!!.submit(Runnable submit@{ val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) @@ -69,27 +85,33 @@ class ActivitiesFragment : BrandedFragment() { lifecycleScope.launch(Dispatchers.IO) { val result = repository.fetchActivities(noteId) withContext(Dispatchers.Main) { - when { - result is ActivitiesResult.Success -> { - // TODO: Render list - result.activities.forEach { - Log.d("DEBUG", "Activity: ${it.type} ${it.message} ${it.datetime}") - } + when (result) { + is ActivitiesResult.Success -> { + adapter.setItems(result.activities) + // TODO: pagination with LastGiven Log.d("DEBUG", "Activities LastGiven is ${result.lastGiven}") } - result is ActivitiesResult.FailedNotModified -> { + + is ActivitiesResult.FailedNotModified -> { // TODO: Show error } - result is ActivitiesResult.Failed -> { + + is ActivitiesResult.Failed -> { // TODO: Show error } } + + binding.progress.visibility = View.GONE } } }) } + override fun onItemClick(activity: Activity) { + TODO("Not yet implemented") + } + companion object { const val ARG_NOTE: String = "NOTE" const val ARG_ACCOUNT: String = "ACCOUNT" diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt new file mode 100644 index 000000000..bf76fd97f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -0,0 +1,86 @@ +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.recyclerview.widget.RecyclerView +import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding + +class ActivityListAdapter ( + protected val context: Context, + private val listener: OnActivityClickListener, + //private val brandingUtil: BrandingUtil +) : RecyclerView.Adapter() { + + private val items = arrayListOf() + + 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) = + (holder as ActivityViewHolder).bind(items[position], listener) + + override fun getItemCount(): Int = items.size + + @SuppressLint("NotifyDataSetChanged") + fun setItems(items: List) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } + + private class ActivityViewHolder(val binding: ActivityListItemBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(activity: Activity, listener: OnActivityClickListener) { + bindDateTime(activity) + bindSubject(activity) + bindMessage(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 + } + } + } + } + + companion object { + private const val TIME_PATTERN = "HH:mm" + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt new file mode 100644 index 000000000..69248f7ba --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -0,0 +1,7 @@ +package it.niedermann.owncloud.notes.activities.adapter + +import it.niedermann.owncloud.notes.activities.model.Activity + +interface OnActivityClickListener { + fun onItemClick(activity: Activity) +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt index 5b98c5c5e..8bd719420 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt @@ -7,17 +7,18 @@ 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 app: String, + val type: String, + val user: String, + val subject: String, val message: String?, val icon: String?, val link: String?, - val datetime: String?, + val datetime: Date, @SerializedName("object_id") val objectId: Long?, @SerializedName("object_name") val objectName: String? ) diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml new file mode 100644 index 000000000..0fa624b92 --- /dev/null +++ b/app/src/main/res/layout/activity_list_item.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index d842dc8ae..73a3ff8af 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -84,4 +84,11 @@ 24dp @dimen/spacer_2x + + 32dp + 32dp + 16dp + 24dp + -3dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d59fb592..84e042770 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -552,10 +552,13 @@ Activities Unable to fetch activities No activity present for this note + Activity Note.md Favorite  • 12:23:45 + This is a placeholder + From 09ff745993130584e28115ede4c42bdce06b8b52 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 16:58:52 +0200 Subject: [PATCH 25/96] Fix Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 4 ++-- app/src/main/res/layout/fragment_activities.xml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 9a9f4fb4d..af32a0a08 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -43,13 +43,13 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { executorService = Executors.newSingleThreadScheduledExecutor() - binding = FragmentActivitiesBinding.inflate(inflater) + binding = FragmentActivitiesBinding.inflate(inflater, container, false) initializeArguments() initializeList() fetchActivities() - return super.onCreateView(inflater, container, savedInstanceState) + return binding.root } private fun initializeArguments() { diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml index b38663af9..db9b7d5dc 100644 --- a/app/src/main/res/layout/fragment_activities.xml +++ b/app/src/main/res/layout/fragment_activities.xml @@ -6,6 +6,7 @@ ~ SPDX-License-Identifier: AGPL-3.0-or-later --> @@ -22,7 +23,8 @@ android:background="@color/bg_default" android:clipToPadding="false" android:scrollbarStyle="outsideOverlay" - android:scrollbars="vertical" /> + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> From 10c016713222657203c85d6208ee4ab277a7e886 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 17:21:00 +0200 Subject: [PATCH 26/96] Empty list Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 23 ++++++++++++++++--- .../activities/adapter/ActivityListAdapter.kt | 6 +++++ app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index af32a0a08..7209d437a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -7,6 +7,7 @@ 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 @@ -87,27 +88,43 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { withContext(Dispatchers.Main) { when (result) { is ActivitiesResult.Success -> { - adapter.setItems(result.activities) + showActivities(result.activities) // TODO: pagination with LastGiven Log.d("DEBUG", "Activities LastGiven is ${result.lastGiven}") } is ActivitiesResult.FailedNotModified -> { - // TODO: Show error + adapter.clear() + showEmptyContent(getString(R.string.activities_no_results)) } is ActivitiesResult.Failed -> { - // TODO: Show error + adapter.clear() + showEmptyContent(getString(R.string.activities_error)) } } binding.progress.visibility = View.GONE + binding.emptyList.visibility = + if (adapter.itemCount > 0) View.VISIBLE + else View.GONE } } }) } + private fun showActivities(activities: List) { + binding.emptyList.visibility = View.GONE + adapter.setItems(activities) + } + + private fun showEmptyContent(message: String) = + with (binding.emptyList) { + visibility = View.VISIBLE + text = message + } + override fun onItemClick(activity: Activity) { TODO("Not yet implemented") } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index bf76fd97f..856c480c0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -43,6 +43,12 @@ class ActivityListAdapter ( notifyDataSetChanged() } + @SuppressLint("NotifyDataSetChanged") + fun clear() { + this.items.clear() + notifyDataSetChanged() + } + private class ActivityViewHolder(val binding: ActivityListItemBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 84e042770..8961e223a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -553,6 +553,8 @@ Unable to fetch activities No activity present for this note Activity + No events like additions, changes and shares yet. + An error occurred loading activities Note.md Favorite From f6ac0e03dcf5b0ccda06b01920bded9e44c8d19b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 17:34:29 +0200 Subject: [PATCH 27/96] Pull-to-refresh Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 9 ++++----- app/src/main/res/layout/fragment_activities.xml | 7 ------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 7209d437a..4a3ab3e89 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -67,17 +67,19 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { 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() { val noteId = note.remoteId ?: return - binding.progress.visibility = View.VISIBLE + binding.swipeContainingList.isRefreshing = true executorService!!.submit(Runnable submit@{ val ssoAccount = @@ -104,10 +106,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { } } - binding.progress.visibility = View.GONE - binding.emptyList.visibility = - if (adapter.itemCount > 0) View.VISIBLE - else View.GONE + binding.swipeContainingList.isRefreshing = false } } diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml index db9b7d5dc..14d95e6ac 100644 --- a/app/src/main/res/layout/fragment_activities.xml +++ b/app/src/main/res/layout/fragment_activities.xml @@ -35,11 +35,4 @@ android:layout_gravity="center" android:text="@string/listview_no_content"/> - - From ab202666413925474cd6ccc3ff5ba0debc38b056 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 18:14:58 +0200 Subject: [PATCH 28/96] List incremental loading Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 30 ++++++++++++------- .../activities/adapter/ActivityListAdapter.kt | 15 ++++++---- .../adapter/OnActivityClickListener.kt | 1 + .../repository/ActivitiesRepository.kt | 2 +- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 4a3ab3e89..b1b77a606 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -38,6 +38,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { 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 @@ -76,7 +77,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) } - private fun fetchActivities() { + private fun fetchActivities(reqLastGiven: Long? = null) { val noteId = note.remoteId ?: return binding.swipeContainingList.isRefreshing = true @@ -86,18 +87,17 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) val repository = ActivitiesRepository(requireContext(), ssoAccount) lifecycleScope.launch(Dispatchers.IO) { - val result = repository.fetchActivities(noteId) + val result = repository.fetchActivities(noteId, reqLastGiven) withContext(Dispatchers.Main) { when (result) { is ActivitiesResult.Success -> { - showActivities(result.activities) - // TODO: pagination with LastGiven - Log.d("DEBUG", "Activities LastGiven is ${result.lastGiven}") + val isFirstPage = reqLastGiven == null || lastGiven == null + lastGiven = result.lastGiven + showActivities(result.activities, isFirstPage) } is ActivitiesResult.FailedNotModified -> { - adapter.clear() - showEmptyContent(getString(R.string.activities_no_results)) + Log.i(TAG, "Reached end of list (last page starting from item $lastGiven") } is ActivitiesResult.Failed -> { @@ -113,9 +113,10 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { }) } - private fun showActivities(activities: List) { + private fun showActivities(activities: List, isFirstPage: Boolean) { binding.emptyList.visibility = View.GONE - adapter.setItems(activities) + if (isFirstPage) adapter.clear() + adapter.addItems(activities) } private fun showEmptyContent(message: String) = @@ -128,9 +129,16 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { TODO("Not yet implemented") } + override fun onEndOfListReached() { + if (adapter.itemCount < ActivitiesRepository.PAGE_SIZE) + return + fetchActivities(lastGiven) + } + companion object { - const val ARG_NOTE: String = "NOTE" - const val ARG_ACCOUNT: String = "ACCOUNT" + 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() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 856c480c0..5c3c27d3f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -31,16 +31,19 @@ class ActivityListAdapter ( ) ) - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = + 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 - @SuppressLint("NotifyDataSetChanged") - fun setItems(items: List) { - this.items.clear() - this.items.addAll(items) - notifyDataSetChanged() + fun addItems(it: List) { + val prevItems = itemCount + this.items.addAll(it) + notifyItemRangeInserted(prevItems, it.size) } @SuppressLint("NotifyDataSetChanged") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt index 69248f7ba..dc0c92d41 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -4,4 +4,5 @@ import it.niedermann.owncloud.notes.activities.model.Activity interface OnActivityClickListener { fun onItemClick(activity: Activity) + fun onEndOfListReached() } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 68c3900da..72ecdbe8e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -30,7 +30,7 @@ class ActivitiesRepository( } companion object { - private const val PAGE_SIZE = 50 + const val PAGE_SIZE = 50 private const val HEADER_LAST_GIVEN = "X-Activity-Last-Given" } From 6bdc7bfc8c8473aee1754c5895b39747e4dcba83 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 18:41:09 +0200 Subject: [PATCH 29/96] WIP icons in activities list Signed-off-by: daniele-verducci --- .../activities/adapter/ActivityListAdapter.kt | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 5c3c27d3f..53102c56e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -7,14 +7,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding class ActivityListAdapter ( - protected val context: Context, - private val listener: OnActivityClickListener, - //private val brandingUtil: BrandingUtil + private val context: Context, + private val listener: OnActivityClickListener ) : RecyclerView.Adapter() { private val items = arrayListOf() @@ -52,13 +52,16 @@ class ActivityListAdapter ( notifyDataSetChanged() } - private class ActivityViewHolder(val binding: ActivityListItemBinding) : - RecyclerView.ViewHolder(binding.root) { + 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) } } @@ -85,9 +88,31 @@ class ActivityListAdapter ( } } + private fun bindIcon(activity: Activity) { + if (activity.icon.isNullOrEmpty()) { + return + } + + Glide + .with(binding.icon.context) + .load(activity.icon) + .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" } } From 819217b67252c3ad804276718e36ff8acf4238ad Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 16:16:42 +0200 Subject: [PATCH 30/96] Prevent Glide to rewrite the url and point to the WebDAV storage for activities icons Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/adapter/ActivityListAdapter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 53102c56e..5d2978dd4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -6,6 +6,7 @@ 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 @@ -95,7 +96,7 @@ class ActivityListAdapter ( Glide .with(binding.icon.context) - .load(activity.icon) + .load(activity.icon.toUri()) .placeholder( R.drawable.ic_activity) .error(R.drawable.ic_activity) .into(binding.icon) From 4257a82101dfc00626f006b0872b6cdb70a64251 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 16:23:05 +0200 Subject: [PATCH 31/96] Converted CustomAppGlideModule to kotlin Signed-off-by: daniele-verducci --- .../shared/util/CustomAppGlideModule.java | 43 ------------------- .../notes/shared/util/CustomAppGlideModule.kt | 40 +++++++++++++++++ 2 files changed, 40 insertions(+), 43 deletions(-) delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java deleted file mode 100644 index 53b3e8f2a..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.niedermann.owncloud.notes.shared.util; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.UiThread; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.AppGlideModule; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@GlideModule -public class CustomAppGlideModule extends AppGlideModule { - - private static final String TAG = CustomAppGlideModule.class.getSimpleName(); - private static final ExecutorService clearDiskCacheExecutor = Executors.newSingleThreadExecutor(); - - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - super.registerComponents(context, glide, registry); - } - - @UiThread - public static void clearCache(@NonNull Context context) { - Log.i(TAG, "Clearing Glide memory cache"); - Glide.get(context).clearMemory(); - clearDiskCacheExecutor.submit(() -> { - Log.i(TAG, "Clearing Glide disk cache"); - Glide.get(context.getApplicationContext()).clearDiskCache(); - }); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt new file mode 100644 index 000000000..a2ee3c83c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.shared.util + +import android.content.Context +import android.util.Log +import androidx.annotation.UiThread +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@GlideModule +class CustomAppGlideModule : AppGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + super.registerComponents(context, glide, registry) + } + + companion object { + private val TAG: String = CustomAppGlideModule::class.java.getSimpleName() + private val clearDiskCacheExecutor: ExecutorService = Executors.newSingleThreadExecutor() + + @JvmStatic + @UiThread + fun clearCache(context: Context) { + Log.i(TAG, "Clearing Glide memory cache") + Glide.get(context).clearMemory() + clearDiskCacheExecutor.submit(Runnable { + Log.i(TAG, "Clearing Glide disk cache") + Glide.get(context.getApplicationContext()).clearDiskCache() + }) + } + } +} \ No newline at end of file From b5994030a1cac4d67a27d7d21b592f33cfd9f65b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:17:39 +0200 Subject: [PATCH 32/96] More kotlin-idiomatic CustomAppGlideModule Signed-off-by: daniele-verducci --- .../owncloud/notes/shared/util/CustomAppGlideModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt index a2ee3c83c..01f7f1e15 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt @@ -31,10 +31,10 @@ class CustomAppGlideModule : AppGlideModule() { fun clearCache(context: Context) { Log.i(TAG, "Clearing Glide memory cache") Glide.get(context).clearMemory() - clearDiskCacheExecutor.submit(Runnable { + clearDiskCacheExecutor.submit { Log.i(TAG, "Clearing Glide disk cache") - Glide.get(context.getApplicationContext()).clearDiskCache() - }) + Glide.get(context.applicationContext).clearDiskCache() + } } } } \ No newline at end of file From 0bc43db0d7357fb6563844070f6436fcffc643f5 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:20:21 +0200 Subject: [PATCH 33/96] Added SVG transcoder to CustomAppGlideModule (note: added dependency caverock.androidsvg, used in Files app and already present in the Notes build because imported by coil-svg in android-common Signed-off-by: daniele-verducci --- app/build.gradle.kts | 1 + .../notes/shared/util/CustomAppGlideModule.kt | 4 +++ .../shared/util/SvgDrawableTranscoder.kt | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e4dbf5231..afcf82c04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,6 +157,7 @@ dependencies { val glideVersion = "5.0.9" implementation("com.github.bumptech.glide:glide:$glideVersion") annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion") + implementation("com.caverock:androidsvg-aar:1.4") // Android X implementation("androidx.appcompat:appcompat:1.8.0") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt index 01f7f1e15..08b800dde 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt @@ -7,12 +7,14 @@ package it.niedermann.owncloud.notes.shared.util import android.content.Context +import android.graphics.drawable.PictureDrawable import android.util.Log import androidx.annotation.UiThread import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule +import com.caverock.androidsvg.SVG import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -20,6 +22,8 @@ import java.util.concurrent.Executors class CustomAppGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { super.registerComponents(context, glide, registry) + + registry.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder()) } companion object { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt new file mode 100644 index 000000000..7631d133b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2014 Google, Inc. All rights reserved. + * SPDX-License-Identifier: BSD-2-Clause + * + * Borrowed from: + * https://github.com/bumptech/glide/blob/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg/ + * SvgDrawableTranscoder.java + */ +package it.niedermann.owncloud.notes.shared.util + +import android.graphics.Picture +import android.graphics.drawable.PictureDrawable +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder +import com.caverock.androidsvg.SVG + +/** + * Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]). + */ +class SvgDrawableTranscoder : ResourceTranscoder { + override fun transcode(toTranscode: Resource, options: Options): Resource { + val svg = toTranscode.get() + val picture = svg.renderToPicture() + val drawable = PictureDrawable(picture) + return SimpleResource(drawable) + } +} From c91ad34387bb053a6819eb7e25e3b0a711c68c40 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:47:52 +0200 Subject: [PATCH 34/96] Added missing SvgDecoder (from Files app) Signed-off-by: daniele-verducci --- .../notes/shared/util/CustomAppGlideModule.kt | 7 +++- .../notes/shared/util/svg/SvgDecoder.kt | 38 +++++++++++++++++++ .../util/{ => svg}/SvgDrawableTranscoder.kt | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDecoder.kt rename app/src/main/java/it/niedermann/owncloud/notes/shared/util/{ => svg}/SvgDrawableTranscoder.kt (95%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt index 08b800dde..b166b2620 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.kt @@ -15,6 +15,9 @@ import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule import com.caverock.androidsvg.SVG +import it.niedermann.owncloud.notes.shared.util.svg.SvgDecoder +import it.niedermann.owncloud.notes.shared.util.svg.SvgDrawableTranscoder +import java.io.InputStream import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -23,7 +26,9 @@ class CustomAppGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { super.registerComponents(context, glide, registry) - registry.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder()) + registry + .register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder()) + .append(InputStream::class.java, SVG::class.java, SvgDecoder()) } companion object { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDecoder.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDecoder.kt new file mode 100644 index 000000000..8d97a7043 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDecoder.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2014 Google, Inc. All rights reserved. + * SPDX-License-Identifier: BSD-2-Clause + * + * Borrowed from: + * https://github.com/bumptech/glide/blob/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg/ + * SvgDecoder.java + */ +package it.niedermann.owncloud.notes.shared.util.svg + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.InputStream + +/** + * Decodes an SVG internal representation from an [InputStream]. + */ +class SvgDecoder : ResourceDecoder { + @Throws(IOException::class) + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource { + try { + val svg = SVG.getFromInputStream(source) + return SimpleResource(svg) + } catch (ex: SVGParseException) { + throw IOException("Cannot load SVG from stream", ex) + } + } + + @Throws(IOException::class) + override fun handles(source: InputStream, options: Options): Boolean = true +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDrawableTranscoder.kt similarity index 95% rename from app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt rename to app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDrawableTranscoder.kt index 7631d133b..7033f5104 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SvgDrawableTranscoder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/svg/SvgDrawableTranscoder.kt @@ -8,7 +8,7 @@ * https://github.com/bumptech/glide/blob/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg/ * SvgDrawableTranscoder.java */ -package it.niedermann.owncloud.notes.shared.util +package it.niedermann.owncloud.notes.shared.util.svg import android.graphics.Picture import android.graphics.drawable.PictureDrawable From b1e6e5aab7b2ffa3591847da2180a0bbe4fa39e5 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:50:37 +0200 Subject: [PATCH 35/96] Fixed Glide module not loaded since it was converted to Kotlin few commits ago (replicated KSP config from Files app) Signed-off-by: daniele-verducci --- app/build.gradle.kts | 3 ++- build.gradle | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index afcf82c04..4e4d11cf0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { id("com.android.application") + id("com.google.devtools.ksp") } val kotlinVersion = rootProject.extra["kotlinVersion"] as String @@ -156,7 +157,7 @@ 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 diff --git a/build.gradle b/build.gradle index c0b12026b..e2781d93b 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:9.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.12" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 75b83128772270580755a388855a549fb93f630f Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 14:34:29 +0200 Subject: [PATCH 36/96] Implemented Versions API (note: added dependency jackrabbit-webdav, already present in the Notes build because imported by android-library for use in RemoteOperation) Signed-off-by: daniele-verducci --- app/build.gradle.kts | 1 + .../activities/repository/VersionsResult.kt | 10 +++ .../notes/persistence/ApiProvider.java | 14 ++++ .../notes/persistence/sync/VersionsAPI.kt | 73 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4e4d11cf0..1dea2379a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -131,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.1") { exclude(group = "org.ogce", module = "xpp3") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt new file mode 100644 index 000000000..2ecf814e4 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt @@ -0,0 +1,10 @@ +package it.niedermann.owncloud.notes.activities.repository + +import com.owncloud.android.lib.resources.files.model.FileVersion + +sealed class VersionsResult { + + data class Success(val versions: List) : VersionsResult() + data object Failed : VersionsResult() + +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 4880d934b..16dc2bf47 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -33,6 +33,7 @@ import it.niedermann.owncloud.notes.persistence.sync.OcsAPI; import it.niedermann.owncloud.notes.persistence.sync.ShareAPI; import it.niedermann.owncloud.notes.persistence.sync.UserStatusAPI; +import it.niedermann.owncloud.notes.persistence.sync.VersionsAPI; import it.niedermann.owncloud.notes.shared.model.ApiVersion; import it.niedermann.owncloud.notes.shared.model.Capabilities; import retrofit2.NextcloudRetrofitApiBuilder; @@ -54,6 +55,7 @@ public class ApiProvider { private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/"; private static final String API_ENDPOINT_FILES_SHARING ="/ocs/v2.php/apps/files_sharing/api/v1/"; private static final String API_ENDPOINT_ACTIVITY = "/ocs/v2.php/apps/activity/api/v2/"; + private static final String API_ENDPOINT_DAV = "/remote.php/dav/"; private static final Map API_CACHE = new ConcurrentHashMap<>(); @@ -63,6 +65,7 @@ public class ApiProvider { private static final Map API_CACHE_FILES = new ConcurrentHashMap<>(); private static final Map API_CACHE_FILES_SHARING = new ConcurrentHashMap<>(); private static final Map API_CACHE_ACTIVITIES = new ConcurrentHashMap<>(); + private static final Map API_CACHE_VERSIONS = new ConcurrentHashMap<>(); public static ApiProvider getInstance() { @@ -157,6 +160,15 @@ public synchronized ActivitiesAPI getActivitiesAPI(@NonNull Context context, @No return api; } + public synchronized VersionsAPI getVersionsAPI(@NonNull SingleSignOnAccount ssoAccount) { + if (API_CACHE_VERSIONS.containsKey(ssoAccount.name)) { + return API_CACHE_VERSIONS.get(ssoAccount.name); + } + final var api = new VersionsAPI(ssoAccount, API_ENDPOINT_DAV); + API_CACHE_VERSIONS.put(ssoAccount.name, api); + return api; + } + /** * Invalidates the API cache for the given {@param ssoAccount} * @@ -176,6 +188,7 @@ public synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAcco API_CACHE_USER_STATUS.remove(ssoAccount.name); API_CACHE_FILES_SHARING.remove(ssoAccount.name); API_CACHE_ACTIVITIES.remove(ssoAccount.name); + API_CACHE_VERSIONS.remove(ssoAccount.name); } /** @@ -197,5 +210,6 @@ public synchronized void invalidateAPICache() { API_CACHE_USER_STATUS.clear(); API_CACHE_FILES_SHARING.clear(); API_CACHE_ACTIVITIES.clear(); + API_CACHE_VERSIONS.clear(); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt new file mode 100644 index 000000000..c9c17bcdd --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -0,0 +1,73 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.persistence.sync + +import com.nextcloud.android.sso.model.SingleSignOnAccount +import com.owncloud.android.lib.common.network.WebdavEntry +import com.owncloud.android.lib.common.network.WebdavUtils +import com.owncloud.android.lib.resources.files.model.FileVersion +import it.niedermann.owncloud.notes.activities.repository.VersionsResult +import org.apache.commons.httpclient.HttpClient +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.DavConstants +import org.apache.jackrabbit.webdav.MultiStatus +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod + + +class VersionsAPI ( + private val ssoAccount: SingleSignOnAccount, + private val davEndpoint: String +) { + fun fetchVersions(fileId: Long) : VersionsResult { + val uri = "${getDavPath()}/versions/${ssoAccount.userId}/versions/$fileId" + val propSet = WebdavUtils.getFileVersionPropSet() + + val query = PropFindMethod(uri, propSet, DavConstants.DEPTH_1) + val status: Int = HttpClient().executeMethod(query) + + // check and process response + val isSuccess = (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) + + if (isSuccess) { + // get data from remote folder + val dataInServer: MultiStatus = query.getResponseBodyAsMultiStatus() + val versions = readData(dataInServer, fileId) + return VersionsResult.Success(versions) + } + + return VersionsResult.Failed + } + + /** + * Read the data retrieved from the server about the file versions. + * + * @param remoteData Full response got from the server with the version data. + * @param fileId File id the versions refer to + */ + private fun readData(remoteData: MultiStatus, fileId: Long): List { + val responses = remoteData.responses + val versions = arrayListOf() + + // parse data from remote folder + val splitElement = getDavPath() + + // loop to update every child + for (resp in responses) { + versions.add( + FileVersion( + fileId, + WebdavEntry(resp, splitElement) + ) + ) + } + return versions + } + + private fun getDavPath(): String { + return "${ssoAccount.url}$davEndpoint" + } +} From e8f7d582034d1f4247d067c090e1bd89ec1aa080 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 15:02:58 +0200 Subject: [PATCH 37/96] Implemented Versions repo Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 31 +++++++++++++++++++ .../repository/VersionsRepository.kt | 17 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index b1b77a606..5e9814835 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -13,6 +13,8 @@ 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 @@ -51,6 +53,10 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { initializeList() fetchActivities() + //DEBUG + fetchVersions() + //DEBUG + return binding.root } @@ -113,6 +119,31 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { }) } + private fun fetchVersions() { + val noteId = note.remoteId ?: return + executorService!!.submit(Runnable submit@{ + val ssoAccount = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) + val repository = VersionsRepository(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, isFirstPage: Boolean) { binding.emptyList.visibility = View.GONE if (isFirstPage) adapter.clear() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt new file mode 100644 index 000000000..8db993e85 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -0,0 +1,17 @@ +package it.niedermann.owncloud.notes.activities.repository + +import android.content.Context +import com.nextcloud.android.sso.model.SingleSignOnAccount +import it.niedermann.owncloud.notes.persistence.ApiProvider +import it.niedermann.owncloud.notes.persistence.sync.VersionsAPI + +class VersionsRepository ( + private val ssoAccount: SingleSignOnAccount +) { + private val api by lazy { ApiProvider.getInstance().getVersionsAPI(ssoAccount) } + + fun fetchVersions(noteId: Long): VersionsResult { + return api.fetchVersions(noteId) + } + +} \ No newline at end of file From d1b00082c2bbb3e81f8858fc41997563bd340182 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 15:50:26 +0200 Subject: [PATCH 38/96] Fixed Versions call failing because unauthenticated Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 2 +- .../repository/VersionsRepository.kt | 3 +- .../notes/persistence/ApiProvider.java | 4 +- .../notes/persistence/sync/VersionsAPI.kt | 41 ++++++++++++------- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 5e9814835..373adf76b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -124,7 +124,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { executorService!!.submit(Runnable submit@{ val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) - val repository = VersionsRepository(ssoAccount) + val repository = VersionsRepository(requireContext(), ssoAccount) lifecycleScope.launch(Dispatchers.IO) { val result = repository.fetchVersions(noteId) withContext(Dispatchers.Main) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index 8db993e85..07f89a092 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -6,9 +6,10 @@ import it.niedermann.owncloud.notes.persistence.ApiProvider import it.niedermann.owncloud.notes.persistence.sync.VersionsAPI class VersionsRepository ( + private val context: Context, private val ssoAccount: SingleSignOnAccount ) { - private val api by lazy { ApiProvider.getInstance().getVersionsAPI(ssoAccount) } + private val api by lazy { ApiProvider.getInstance().getVersionsAPI(context, ssoAccount) } fun fetchVersions(noteId: Long): VersionsResult { return api.fetchVersions(noteId) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 16dc2bf47..d7cd26067 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -160,11 +160,11 @@ public synchronized ActivitiesAPI getActivitiesAPI(@NonNull Context context, @No return api; } - public synchronized VersionsAPI getVersionsAPI(@NonNull SingleSignOnAccount ssoAccount) { + public synchronized VersionsAPI getVersionsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { if (API_CACHE_VERSIONS.containsKey(ssoAccount.name)) { return API_CACHE_VERSIONS.get(ssoAccount.name); } - final var api = new VersionsAPI(ssoAccount, API_ENDPOINT_DAV); + final var api = new VersionsAPI(getNextcloudAPI(context, ssoAccount), ssoAccount, API_ENDPOINT_DAV); API_CACHE_VERSIONS.put(ssoAccount.name, api); return api; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index c9c17bcdd..883ab60c3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -6,19 +6,23 @@ */ package it.niedermann.owncloud.notes.persistence.sync +import com.nextcloud.android.sso.aidl.NextcloudRequest +import com.nextcloud.android.sso.api.NextcloudAPI +import com.nextcloud.android.sso.api.Response import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.activities.repository.VersionsResult -import org.apache.commons.httpclient.HttpClient -import org.apache.commons.httpclient.HttpStatus import org.apache.jackrabbit.webdav.DavConstants import org.apache.jackrabbit.webdav.MultiStatus import org.apache.jackrabbit.webdav.client.methods.PropFindMethod +import org.apache.jackrabbit.webdav.xml.DomUtil +import java.io.ByteArrayOutputStream class VersionsAPI ( + private val nextcloudAPI: NextcloudAPI, private val ssoAccount: SingleSignOnAccount, private val davEndpoint: String ) { @@ -27,19 +31,23 @@ class VersionsAPI ( val propSet = WebdavUtils.getFileVersionPropSet() val query = PropFindMethod(uri, propSet, DavConstants.DEPTH_1) - val status: Int = HttpClient().executeMethod(query) - // check and process response - val isSuccess = (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) + // We manually build the request to pass it to nextcloudAPI (required for authentication) + val requestBodyOutStr = ByteArrayOutputStream() + query.requestEntity.writeRequest(requestBodyOutStr) + val requestBody = requestBodyOutStr.toString() + val request = NextcloudRequest.Builder() + .setMethod("PROPFIND") + .setUrl(uri) + .setRequestBody(requestBody) + .build() - if (isSuccess) { - // get data from remote folder - val dataInServer: MultiStatus = query.getResponseBodyAsMultiStatus() - val versions = readData(dataInServer, fileId) - return VersionsResult.Success(versions) + return try { + val response = nextcloudAPI.performNetworkRequestV2(request) + VersionsResult.Success(readData(response, fileId)) + } catch (e: Exception) { + VersionsResult.Failed } - - return VersionsResult.Failed } /** @@ -48,8 +56,11 @@ class VersionsAPI ( * @param remoteData Full response got from the server with the version data. * @param fileId File id the versions refer to */ - private fun readData(remoteData: MultiStatus, fileId: Long): List { - val responses = remoteData.responses + private fun readData(response: Response, fileId: Long): List { + // Use jackrabbit tp parse a webdav MultiStatus from response + val ms = MultiStatus.createFromXml(DomUtil.parseDocument(response.body).documentElement) + val responses = ms.responses + // Each response is a version val versions = arrayListOf() // parse data from remote folder @@ -68,6 +79,6 @@ class VersionsAPI ( } private fun getDavPath(): String { - return "${ssoAccount.url}$davEndpoint" + return davEndpoint } } From 9b9cc448676900be29effedc2ec45309cf317c85 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 11:52:42 +0200 Subject: [PATCH 39/96] Implemented Activity/Versions ordered merging and updated adapter to display both Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 18 ++- .../activities/adapter/ActivityListAdapter.kt | 108 ++++++++++++++---- .../adapter/OnActivityClickListener.kt | 4 +- .../notes/activities/model/Activity.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 5 files changed, 111 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 373adf76b..7f5d7dc34 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope 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 @@ -130,6 +131,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { withContext(Dispatchers.Main) { when (result) { is VersionsResult.Success -> { + showVersions(result.versions) result.versions.forEach { Log.d("DEBUG", "Version: ${it.remoteId} ${it.fileName} ${it.modifiedTimestamp}") } @@ -144,10 +146,15 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { }) } - private fun showActivities(activities: List, isFirstPage: Boolean) { + private fun showActivities(items: List, isFirstPage: Boolean) { binding.emptyList.visibility = View.GONE if (isFirstPage) adapter.clear() - adapter.addItems(activities) + adapter.addActivities(items) + } + + private fun showVersions(items: List) { + binding.emptyList.visibility = View.GONE + adapter.addVersions(items) } private fun showEmptyContent(message: String) = @@ -156,7 +163,12 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { text = message } - override fun onItemClick(activity: Activity) { + override fun onActivityItemClick(activity: Activity) { + // No action + } + + override fun onVersionItemClick(version: FileVersion) { + // Restore the version TODO("Not yet implemented") } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 5d2978dd4..d25fc86df 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.model.Activity import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding @@ -18,7 +19,7 @@ class ActivityListAdapter ( private val listener: OnActivityClickListener ) : RecyclerView.Adapter() { - private val items = arrayListOf() + private val items = arrayListOf() override fun onCreateViewHolder( parent: ViewGroup, @@ -41,10 +42,22 @@ class ActivityListAdapter ( override fun getItemCount(): Int = items.size - fun addItems(it: List) { - val prevItems = itemCount - this.items.addAll(it) - notifyItemRangeInserted(prevItems, it.size) + @SuppressLint("NotifyDataSetChanged") + fun addActivities(items: List) { + this.items.addAll( + items.map { ActivityListItem(context, it) } + ) + this.items.sort() + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun addVersions(items: List) { + this.items.addAll( + items.map { ActivityListItem(context, it) } + ) + this.items.sort() + notifyDataSetChanged() } @SuppressLint("NotifyDataSetChanged") @@ -58,51 +71,58 @@ class ActivityListAdapter ( //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) } + fun bind(item: ActivityListItem, listener: OnActivityClickListener) { + bindDateTime(item) + bindSubject(item) + bindMessage(item) + bindIcon(item) + binding.root.setOnClickListener { + when { + item.activity != null -> listener.onActivityItemClick(item.activity) + item.fileVersion != null -> listener.onVersionItemClick(item.fileVersion) + } + } } - private fun bindDateTime(activity: Activity) { + private fun bindDateTime(activity: ActivityListItem) { binding.datetime.apply { visibility = View.VISIBLE - text = DateFormat.format(TIME_PATTERN, activity.datetime.time) + text = DateFormat.format(TIME_PATTERN, activity.getTimestamp()) } } - private fun bindSubject(activity: Activity) { - binding.subject.text = activity.subject + private fun bindSubject(activity: ActivityListItem) { + binding.subject.text = activity.getTitle() } - private fun bindMessage(activity: Activity) { + private fun bindMessage(activity: ActivityListItem) { binding.message.apply { when { - activity.message.isNullOrEmpty() -> visibility = View.GONE + activity.getMessage().isNullOrEmpty() -> visibility = View.GONE else -> { visibility = View.VISIBLE - text = activity.message + text = activity.getMessage() } } } } - private fun bindIcon(activity: Activity) { - if (activity.icon.isNullOrEmpty()) { + private fun bindIcon(activity: ActivityListItem) { + val iconUrl = activity.getIconUrl() + if (iconUrl.isNullOrEmpty()) { + binding.icon.setImageResource(R.drawable.ic_activity) return } Glide .with(binding.icon.context) - .load(activity.icon.toUri()) + .load(iconUrl.toUri()) .placeholder( R.drawable.ic_activity) .error(R.drawable.ic_activity) .into(binding.icon) /* - if (activity.icon.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) { + if (iconUrl.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) { binding.icon.imageTintList = null } else { brandingUtil.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) @@ -118,4 +138,48 @@ class ActivityListAdapter ( } + + class ActivityListItem : Comparable { + val context: Context + val activity: Activity? + val fileVersion: FileVersion? + + constructor(context: Context, activity: Activity) { + this.context = context + this.activity = activity + this.fileVersion = null + } + + constructor(context: Context, fileVersion: FileVersion) { + this.context = context + this.activity = null + this.fileVersion = fileVersion + } + + fun getTimestamp() : Long = + when { + activity != null -> activity.datetime.time + fileVersion != null -> fileVersion.modifiedTimestamp + else -> 0 + } + + fun getTitle() : String? = + when { + activity != null -> activity.subject + fileVersion != null -> context.getString(R.string.new_version_was_created) + else -> null + } + + fun getMessage() : String? = + when { + activity != null -> activity.message + fileVersion != null -> "${fileVersion.fileLength} B" + else -> null + } + + fun getIconUrl() : String? = activity?.icon + + override fun compareTo(other: ActivityListItem): Int = + getTimestamp().compareTo(other.getTimestamp()) + } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt index dc0c92d41..e3465021d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -1,8 +1,10 @@ package it.niedermann.owncloud.notes.activities.adapter +import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.activities.model.Activity interface OnActivityClickListener { - fun onItemClick(activity: Activity) + fun onActivityItemClick(activity: Activity) + fun onVersionItemClick(version: FileVersion) fun onEndOfListReached() } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt index 8bd719420..7874f4cba 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt @@ -21,4 +21,9 @@ data class Activity( val datetime: Date, @SerializedName("object_id") val objectId: Long?, @SerializedName("object_name") val objectName: String? -) +) : Comparable { + + override fun compareTo(other: Activity): Int = + (datetime.time - other.datetime.time).toInt() + +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8961e223a..83b42ee73 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -555,6 +555,7 @@ Activity No events like additions, changes and shares yet. An error occurred loading activities + New version was created Note.md Favorite From dc1dc200dab0203c41a91dad4ab2fe12e982073b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 11:58:48 +0200 Subject: [PATCH 40/96] Fixed activity pagination (fileVersion endpoint donesn't have pagination, so we must identify the last _activity_ in the list to load the next activities) Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 2 +- .../notes/activities/adapter/ActivityListAdapter.kt | 9 ++++++--- .../notes/activities/adapter/OnActivityClickListener.kt | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 7f5d7dc34..4175f916a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -172,7 +172,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { TODO("Not yet implemented") } - override fun onEndOfListReached() { + override fun lastActivityReached() { if (adapter.itemCount < ActivitiesRepository.PAGE_SIZE) return fetchActivities(lastGiven) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index d25fc86df..700adf17b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -20,6 +20,7 @@ class ActivityListAdapter ( ) : RecyclerView.Adapter() { private val items = arrayListOf() + private var lastActivity: Activity? = null override fun onCreateViewHolder( parent: ViewGroup, @@ -34,10 +35,11 @@ class ActivityListAdapter ( ) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (position == itemCount - 1) { - listener.onEndOfListReached() + val item = items[position] + if (lastActivity != null && item.activity == lastActivity) { + listener.lastActivityReached() } - (holder as ActivityViewHolder).bind(items[position], listener) + (holder as ActivityViewHolder).bind(item, listener) } override fun getItemCount(): Int = items.size @@ -48,6 +50,7 @@ class ActivityListAdapter ( items.map { ActivityListItem(context, it) } ) this.items.sort() + lastActivity = items.last() notifyDataSetChanged() } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt index e3465021d..3682fb505 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -6,5 +6,5 @@ import it.niedermann.owncloud.notes.activities.model.Activity interface OnActivityClickListener { fun onActivityItemClick(activity: Activity) fun onVersionItemClick(version: FileVersion) - fun onEndOfListReached() + fun lastActivityReached() } \ No newline at end of file From 63fb391c3ca41a0a302a554c83745cd657682be1 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 12:18:34 +0200 Subject: [PATCH 41/96] Applied branding color Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 1 + .../activities/adapter/ActivityListAdapter.kt | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 4175f916a..32bf1da88 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -79,6 +79,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { } override fun applyBrand(color: Int) { + adapter.applyBrand(color) val util = BrandingUtil.of(color, requireContext()) util.platform.colorViewBackground(binding.getRoot()) util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 700adf17b..6e300331b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -9,18 +9,23 @@ import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.branding.Branded +import it.niedermann.owncloud.notes.branding.BrandedViewHolder +import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding class ActivityListAdapter ( private val context: Context, private val listener: OnActivityClickListener -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter(), Branded { private val items = arrayListOf() private var lastActivity: Activity? = null + private var brandColor: Int? = null override fun onCreateViewHolder( parent: ViewGroup, @@ -35,11 +40,18 @@ class ActivityListAdapter ( ) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + // Apply brand + val holder = (holder as ActivityViewHolder) + if (brandColor != null) { + holder.applyBrand(brandColor!!) + } + + // Retrieve item and bind vh val item = items[position] if (lastActivity != null && item.activity == lastActivity) { listener.lastActivityReached() } - (holder as ActivityViewHolder).bind(item, listener) + holder.bind(item, listener) } override fun getItemCount(): Int = items.size @@ -69,10 +81,16 @@ class ActivityListAdapter ( notifyDataSetChanged() } + override fun applyBrand(color: Int) { + brandColor = color + } + private class ActivityViewHolder( val binding: ActivityListItemBinding, //val brandingUtil: BrandingUtil - ) : RecyclerView.ViewHolder(binding.root) { + ) : BrandedViewHolder(binding.root) { + + private var brandingUtil: BrandingUtil? = null fun bind(item: ActivityListItem, listener: OnActivityClickListener) { bindDateTime(item) @@ -124,16 +142,18 @@ class ActivityListAdapter ( .error(R.drawable.ic_activity) .into(binding.icon) - /* if (iconUrl.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) { binding.icon.imageTintList = null } else { - brandingUtil.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) + brandingUtil?.platform?.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) } - */ } + override fun applyBrand(color: Int) { + brandingUtil = BrandingUtil.of(color, binding.root.context) + } + companion object { private const val TIME_PATTERN = "HH:mm" private const val COLORED_ICON_SUFFIX = "-color.svg" From 12a469b0748b6ce1680e622be1002001eec85b5c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 12:28:52 +0200 Subject: [PATCH 42/96] List loading/refresh Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 32bf1da88..699499efb 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -52,11 +52,8 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { initializeArguments() initializeList() - fetchActivities() - //DEBUG - fetchVersions() - //DEBUG + fetchData() return binding.root } @@ -75,7 +72,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { this ) binding.list.adapter = adapter - binding.swipeContainingList.setOnRefreshListener { fetchActivities() } + binding.swipeContainingList.setOnRefreshListener { fetchData() } } override fun applyBrand(color: Int) { @@ -85,6 +82,11 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) } + private fun fetchData() { + fetchActivities() + fetchVersions() + } + private fun fetchActivities(reqLastGiven: Long? = null) { val noteId = note.remoteId ?: return From 5c118374c53e550f894f85ff809bbb830e3112c0 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 12:54:15 +0200 Subject: [PATCH 43/96] Restore version action icon Signed-off-by: daniele-verducci --- .../activities/adapter/ActivityListAdapter.kt | 9 +++++ app/src/main/res/drawable/ic_history.xml | 16 +++++++++ .../main/res/layout/activity_list_item.xml | 33 +++++++++++++------ app/src/main/res/values/dimens.xml | 2 ++ 4 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/drawable/ic_history.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 6e300331b..5cbe446d6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -97,6 +97,7 @@ class ActivityListAdapter ( bindSubject(item) bindMessage(item) bindIcon(item) + bindActionIcon(item) binding.root.setOnClickListener { when { item.activity != null -> listener.onActivityItemClick(item.activity) @@ -150,6 +151,12 @@ class ActivityListAdapter ( } + private fun bindActionIcon(activity: ActivityListItem) { + binding.actionIcon.visibility = + if (activity.canBeRestored()) View.VISIBLE + else View.GONE + } + override fun applyBrand(color: Int) { brandingUtil = BrandingUtil.of(color, binding.root.context) } @@ -202,6 +209,8 @@ class ActivityListAdapter ( fun getIconUrl() : String? = activity?.icon + fun canBeRestored() : Boolean = fileVersion != null + override fun compareTo(other: ActivityListItem): Int = getTimestamp().compareTo(other.getTimestamp()) } diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 000000000..4195575e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index 0fa624b92..99448dabb 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -32,19 +32,32 @@ android:layout_toEndOf="@id/icon" android:orientation="vertical"> - + android:paddingEnd="@dimen/zero" > + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 73a3ff8af..4379bc06f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -87,6 +87,8 @@ 32dp 32dp + 24dp + 24dp 16dp 24dp -3dp From 7f03153cc9feefe87c1c9ba2f9d4f15068a3ad84 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 19 Oct 2026 15:49:43 +0200 Subject: [PATCH 44/96] Implemented version restore, but MOVE Dav method is blocked by SSO library Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 22 +++++++++++- .../repository/RestoreVersionsResult.kt | 7 ++++ .../repository/VersionsRepository.kt | 5 +++ .../notes/persistence/sync/VersionsAPI.kt | 35 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 699499efb..9c28b4808 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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 @@ -14,9 +15,11 @@ 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.RestoreVersionsResult 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.BrandedSnackbar import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding import it.niedermann.owncloud.notes.persistence.entity.Account @@ -172,7 +175,24 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { override fun onVersionItemClick(version: FileVersion) { // Restore the version - TODO("Not yet implemented") + val ssoAccount = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) + val repository = VersionsRepository(requireContext(), ssoAccount) + lifecycleScope.launch(Dispatchers.IO) { + val result = repository.restoreVersion(version) + withContext(Dispatchers.Main) { + val message = + if (result == RestoreVersionsResult.Success) + R.string.version_restored + else + R.string.version_restore_failed + + BrandedSnackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() + + // Reload list + if (result == RestoreVersionsResult.Success) fetchData() + } + } } override fun lastActivityReached() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt new file mode 100644 index 000000000..54429e6ab --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt @@ -0,0 +1,7 @@ +package it.niedermann.owncloud.notes.activities.repository + +import com.owncloud.android.lib.resources.files.model.FileVersion + +enum class RestoreVersionsResult{ + Success, Failed +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index 07f89a092..5a17df93e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount +import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.persistence.ApiProvider import it.niedermann.owncloud.notes.persistence.sync.VersionsAPI @@ -15,4 +16,8 @@ class VersionsRepository ( return api.fetchVersions(noteId) } + fun restoreVersion(version: FileVersion): RestoreVersionsResult { + return api.restoreVersion(version) + } + } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index 883ab60c3..d3b2486be 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -6,6 +6,8 @@ */ package it.niedermann.owncloud.notes.persistence.sync +import android.net.Uri +import android.util.Log import com.nextcloud.android.sso.aidl.NextcloudRequest import com.nextcloud.android.sso.api.NextcloudAPI import com.nextcloud.android.sso.api.Response @@ -13,6 +15,7 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.lib.resources.files.model.FileVersion +import it.niedermann.owncloud.notes.activities.repository.RestoreVersionsResult import it.niedermann.owncloud.notes.activities.repository.VersionsResult import org.apache.jackrabbit.webdav.DavConstants import org.apache.jackrabbit.webdav.MultiStatus @@ -27,7 +30,7 @@ class VersionsAPI ( private val davEndpoint: String ) { fun fetchVersions(fileId: Long) : VersionsResult { - val uri = "${getDavPath()}/versions/${ssoAccount.userId}/versions/$fileId" + val uri = "${getDavPath()}versions/${ssoAccount.userId}/versions/$fileId" val propSet = WebdavUtils.getFileVersionPropSet() val query = PropFindMethod(uri, propSet, DavConstants.DEPTH_1) @@ -50,6 +53,32 @@ class VersionsAPI ( } } + fun restoreVersion(version: FileVersion): RestoreVersionsResult { + // Version restore is a move from the versions file path to the original one + val versionsPath = "${getDavPath()}versions/${ssoAccount.userId}" + val source = "$versionsPath/versions/${version.localId}/${Uri.encode(version.fileName)}" + val destination = "$versionsPath/restore/${version.localId}" + val request = NextcloudRequest.Builder() + .setMethod("MOVE") + .setUrl(source) + .setHeader( + mapOf( + "Destination" to listOf(destination), + "Overwrite" to listOf("T") // T = True + ) + ) + .build() + + try { + val response = nextcloudAPI.performNetworkRequestV2(request) + Log.i(TAG, response.toString()) + return RestoreVersionsResult.Success + } catch (e: Exception) { + Log.e(TAG, "restoreVersion failed: ${e.message}") + return RestoreVersionsResult.Failed + } + } + /** * Read the data retrieved from the server about the file versions. * @@ -81,4 +110,8 @@ class VersionsAPI ( private fun getDavPath(): String { return davEndpoint } + + companion object { + val TAG = "VersionsAPI" + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83b42ee73..dff100fcb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -556,6 +556,8 @@ No events like additions, changes and shares yet. An error occurred loading activities New version was created + Version restored + Could not restore version Note.md Favorite From 929274d65f3959ea0d6c79f65722f04169c12ba1 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 20 Oct 2026 17:09:31 +0200 Subject: [PATCH 45/96] Updated MOVE WebDAV request call to SSO lib, user-readable error in case of unsupported Nextcloud Files version Signed-off-by: daniele-verducci --- .../activities/repository/RestoreVersionsResult.kt | 2 +- .../owncloud/notes/persistence/sync/VersionsAPI.kt | 12 ++++++------ app/src/main/res/values/strings.xml | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt index 54429e6ab..f4b8b236d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt @@ -3,5 +3,5 @@ package it.niedermann.owncloud.notes.activities.repository import com.owncloud.android.lib.resources.files.model.FileVersion enum class RestoreVersionsResult{ - Success, Failed + Success, Failed, FailedUnsupported } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index d3b2486be..835720f55 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -11,6 +11,7 @@ import android.util.Log import com.nextcloud.android.sso.aidl.NextcloudRequest import com.nextcloud.android.sso.api.NextcloudAPI import com.nextcloud.android.sso.api.Response +import com.nextcloud.android.sso.exceptions.NextcloudUnsupportedMethodException import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils @@ -61,18 +62,17 @@ class VersionsAPI ( val request = NextcloudRequest.Builder() .setMethod("MOVE") .setUrl(source) - .setHeader( - mapOf( - "Destination" to listOf(destination), - "Overwrite" to listOf("T") // T = True - ) - ) + .setAdditionalParameter(NextcloudRequest.ADDITIONAL_PARAMETER_MOVE_DESTINATION, destination) + .setAdditionalParameter(NextcloudRequest.ADDITIONAL_PARAMETER_MOVE_OVERWRITE, true.toString()) .build() try { val response = nextcloudAPI.performNetworkRequestV2(request) Log.i(TAG, response.toString()) return RestoreVersionsResult.Success + } catch (e: NextcloudUnsupportedMethodException) { + Log.e(TAG, "restoreVersion used an unsupported method: ${e.message}") + return RestoreVersionsResult.FailedUnsupported } catch (e: Exception) { Log.e(TAG, "restoreVersion failed: ${e.message}") return RestoreVersionsResult.Failed diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dff100fcb..b1a90f237 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -557,6 +557,7 @@ An error occurred loading activities New version was created Version restored + Version restore not supported: please upgrade Nextcloud Files app Could not restore version Note.md From f21f785b44526ebbed17b5557d163500edcb8075 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 20 Oct 2026 19:38:38 +0200 Subject: [PATCH 46/96] Refactored entire data fetch and presentation --- .../activities/ActivitiesDataProvider.kt | 74 ++++++++++ .../notes/activities/ActivitiesFragment.kt | 137 +++++------------- .../activities/adapter/ActivitiesData.kt | 24 +++ .../activities/adapter/ActivityListAdapter.kt | 73 +--------- .../activities/adapter/ActivityListItem.kt | 52 +++++++ .../repository/ActivitiesRepository.kt | 6 +- .../activities/repository/ActivitiesResult.kt | 4 - .../notes/persistence/sync/VersionsAPI.kt | 1 + 8 files changed, 191 insertions(+), 180 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt new file mode 100644 index 000000000..909494bc3 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -0,0 +1,74 @@ +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 fetchNextPage() { + // Versions are non-paginated: fetch only once + val fetchVersions = data.isEmpty() + job?.cancel() + job = lifecycleScope.launch { + val (activitiesResult, versionsResult) = withContext(Dispatchers.IO) { + val activities = async { activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) } + val versions = async { + if (fetchVersions) versionsRepo.fetchVersions(noteId) + else VersionsResult.Success(listOf()) + } + activities.await() to versions.await() + } + + when { + activitiesResult is ActivitiesResult.Success && versionsResult is VersionsResult.Success -> { + data.activitiesLastGiven = activitiesResult.lastGiven + data.activities.addAll(activitiesResult.activities) + data.versions.addAll(versionsResult.versions) + listener.onPageFetched() + return@launch + } + + activitiesResult is ActivitiesResult.Failed -> Log.e(TAG, "Fetching activities failed") + + versionsResult is VersionsResult.Failed -> Log.e(TAG, "Fetching versions failed") + } + + listener.onPageFetchError() + } + } + + fun reset() { + data.activities.clear() + data.versions.clear() + data.activitiesLastGiven = null + } + + interface OnPageFetchedListener { + fun onPageFetched() + fun onPageFetchError() + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 9c28b4808..3bd7bebbd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -1,7 +1,7 @@ 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 @@ -14,10 +14,8 @@ 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.RestoreVersionsResult 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.BrandedSnackbar import it.niedermann.owncloud.notes.branding.BrandingUtil @@ -28,8 +26,6 @@ import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgume 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 @@ -38,26 +34,21 @@ import java.util.concurrent.ScheduledExecutorService * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { +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 var lastGiven: Long? = null - - private var executorService: ScheduledExecutorService? = null + private lateinit var activityDataProvider: ActivitiesDataProvider override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - executorService = Executors.newSingleThreadScheduledExecutor() binding = FragmentActivitiesBinding.inflate(inflater, container, false) initializeArguments() initializeList() - fetchData() - return binding.root } @@ -70,12 +61,24 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { } private fun initializeList() { + activityDataProvider = ActivitiesDataProvider( + requireContext(), + lifecycleScope, + note.id, + this + ) adapter = ActivityListAdapter( requireContext(), this ) binding.list.adapter = adapter - binding.swipeContainingList.setOnRefreshListener { fetchData() } + binding.swipeContainingList.setOnRefreshListener { + // Reload list + activityDataProvider.reset() + activityDataProvider.fetchNextPage() + } + + activityDataProvider.fetchNextPage() } override fun applyBrand(color: Int) { @@ -85,90 +88,6 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) } - private fun fetchData() { - fetchActivities() - fetchVersions() - } - - 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 -> { - showVersions(result.versions) - result.versions.forEach { - Log.d("DEBUG", "Version: ${it.remoteId} ${it.fileName} ${it.modifiedTimestamp}") - } - } - - is VersionsResult.Failed -> { - Log.e("DEBUG", "Versions failed") - } - } - } - } - }) - } - - private fun showActivities(items: List, isFirstPage: Boolean) { - binding.emptyList.visibility = View.GONE - if (isFirstPage) adapter.clear() - adapter.addActivities(items) - } - - private fun showVersions(items: List) { - binding.emptyList.visibility = View.GONE - adapter.addVersions(items) - } - - private fun showEmptyContent(message: String) = - with (binding.emptyList) { - visibility = View.VISIBLE - text = message - } - override fun onActivityItemClick(activity: Activity) { // No action } @@ -190,7 +109,10 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { BrandedSnackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() // Reload list - if (result == RestoreVersionsResult.Success) fetchData() + if (result == RestoreVersionsResult.Success) { + activityDataProvider.reset() + activityDataProvider.fetchNextPage() + } } } } @@ -198,7 +120,24 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener { override fun lastActivityReached() { if (adapter.itemCount < ActivitiesRepository.PAGE_SIZE) return - fetchActivities(lastGiven) + 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.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 { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt new file mode 100644 index 000000000..2c48ccdcb --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -0,0 +1,24 @@ +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() + val versions = arrayListOf() + var activitiesLastGiven: Long? = null + + fun getAggregatedList(context: Context) : List { + val items = arrayListOf() + items.addAll( + activities.map { ActivityListItem(context, it) } + ) + items.addAll( + versions.map { ActivityListItem(context, it) } + ) + return items + } + + fun isEmpty() : Boolean = activities.isEmpty() && versions.isEmpty() +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 5cbe446d6..323bce0e8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -23,7 +23,7 @@ class ActivityListAdapter ( private val listener: OnActivityClickListener ) : RecyclerView.Adapter(), Branded { - private val items = arrayListOf() + val items = arrayListOf() private var lastActivity: Activity? = null private var brandColor: Int? = null @@ -56,31 +56,6 @@ class ActivityListAdapter ( override fun getItemCount(): Int = items.size - @SuppressLint("NotifyDataSetChanged") - fun addActivities(items: List) { - this.items.addAll( - items.map { ActivityListItem(context, it) } - ) - this.items.sort() - lastActivity = items.last() - notifyDataSetChanged() - } - - @SuppressLint("NotifyDataSetChanged") - fun addVersions(items: List) { - this.items.addAll( - items.map { ActivityListItem(context, it) } - ) - this.items.sort() - notifyDataSetChanged() - } - - @SuppressLint("NotifyDataSetChanged") - fun clear() { - this.items.clear() - notifyDataSetChanged() - } - override fun applyBrand(color: Int) { brandColor = color } @@ -168,50 +143,4 @@ class ActivityListAdapter ( } - - class ActivityListItem : Comparable { - val context: Context - val activity: Activity? - val fileVersion: FileVersion? - - constructor(context: Context, activity: Activity) { - this.context = context - this.activity = activity - this.fileVersion = null - } - - constructor(context: Context, fileVersion: FileVersion) { - this.context = context - this.activity = null - this.fileVersion = fileVersion - } - - fun getTimestamp() : Long = - when { - activity != null -> activity.datetime.time - fileVersion != null -> fileVersion.modifiedTimestamp - else -> 0 - } - - fun getTitle() : String? = - when { - activity != null -> activity.subject - fileVersion != null -> context.getString(R.string.new_version_was_created) - else -> null - } - - fun getMessage() : String? = - when { - activity != null -> activity.message - fileVersion != null -> "${fileVersion.fileLength} B" - else -> null - } - - fun getIconUrl() : String? = activity?.icon - - fun canBeRestored() : Boolean = fileVersion != null - - override fun compareTo(other: ActivityListItem): Int = - getTimestamp().compareTo(other.getTimestamp()) - } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt new file mode 100644 index 000000000..c1f6811a4 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt @@ -0,0 +1,52 @@ +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.R +import it.niedermann.owncloud.notes.activities.model.Activity + +class ActivityListItem : Comparable { + val context: Context + val activity: Activity? + val fileVersion: FileVersion? + + constructor(context: Context, activity: Activity) { + this.context = context + this.activity = activity + this.fileVersion = null + } + + constructor(context: Context, fileVersion: FileVersion) { + this.context = context + this.activity = null + this.fileVersion = fileVersion + } + + fun getTimestamp() : Long = + when { + activity != null -> activity.datetime.time + fileVersion != null -> fileVersion.modifiedTimestamp + else -> 0 + } + + fun getTitle() : String? = + when { + activity != null -> activity.subject + fileVersion != null -> context.getString(R.string.new_version_was_created) + else -> null + } + + fun getMessage() : String? = + when { + activity != null -> activity.message + fileVersion != null -> "${fileVersion.fileLength} B" + else -> null + } + + fun getIconUrl() : String? = activity?.icon + + fun canBeRestored() : Boolean = fileVersion != null + + override fun compareTo(other: ActivityListItem): Int = + getTimestamp().compareTo(other.getTimestamp()) +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 72ecdbe8e..c3d8b6c85 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -15,17 +15,13 @@ class ActivitiesRepository( fun fetchActivities(noteId: Long, since: Long? = null): ActivitiesResult { val response = api.fetchActivities(noteId, since, PAGE_SIZE).execute() - if (response.isSuccessful) { + if (response.isSuccessful || response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { return ActivitiesResult.Success( response.body()?.ocs?.data ?: listOf(), response.headers()[HEADER_LAST_GIVEN]?.toLongOrNull() ) } - if (response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { - return ActivitiesResult.FailedNotModified() - } - return ActivitiesResult.Failed() } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt index d2cd6b282..7fe60a31d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -7,10 +7,6 @@ sealed class ActivitiesResult(open val descriptionId: Int? = null) { data class Success(val activities: List, val lastGiven: Long?) : ActivitiesResult() - data class FailedNotModified( - override val descriptionId: Int = R.string.activities_fetch_failed_not_modified - ) : ActivitiesResult(descriptionId) - data class Failed( override val descriptionId: Int = R.string.activities_fetch_failed ) : ActivitiesResult(descriptionId) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index 835720f55..9adbd9603 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -50,6 +50,7 @@ class VersionsAPI ( val response = nextcloudAPI.performNetworkRequestV2(request) VersionsResult.Success(readData(response, fileId)) } catch (e: Exception) { + Log.e(TAG, "Unable to fetch versions: $e") VersionsResult.Failed } } From fd126d2dce47b4ba64bde3410ac54283f56f8423 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 20 Oct 2026 19:45:03 +0200 Subject: [PATCH 47/96] Fixes Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 9 ++++++++- .../owncloud/notes/activities/adapter/ActivitiesData.kt | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 3bd7bebbd..cfafa344f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -2,6 +2,7 @@ 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 @@ -61,10 +62,16 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie } 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, - note.id, + id, this ) adapter = ActivityListAdapter( diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index 2c48ccdcb..596b4fde0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -17,6 +17,7 @@ class ActivitiesData { items.addAll( versions.map { ActivityListItem(context, it) } ) + items.sort() return items } From 18dbd599ba2dec31bf1f2f840904fa659c4bcef6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 21 Oct 2026 09:44:58 +0200 Subject: [PATCH 48/96] Same order as web version, fixed pagination Signed-off-by: daniele-verducci --- .../niedermann/owncloud/notes/activities/ActivitiesFragment.kt | 2 +- .../owncloud/notes/activities/adapter/ActivitiesData.kt | 2 +- .../owncloud/notes/activities/adapter/ActivityListAdapter.kt | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index cfafa344f..bb2107b8b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -75,7 +75,6 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie this ) adapter = ActivityListAdapter( - requireContext(), this ) binding.list.adapter = adapter @@ -138,6 +137,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie adapter.items.addAll( activityDataProvider.data.getAggregatedList(requireContext()) ) + adapter.lastActivity = activityDataProvider.data.activities.lastOrNull() adapter.notifyDataSetChanged() } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index 596b4fde0..4c05f49c5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -17,7 +17,7 @@ class ActivitiesData { items.addAll( versions.map { ActivityListItem(context, it) } ) - items.sort() + items.sortDescending() return items } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 323bce0e8..6fe17470e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -19,12 +19,11 @@ import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding class ActivityListAdapter ( - private val context: Context, private val listener: OnActivityClickListener ) : RecyclerView.Adapter(), Branded { val items = arrayListOf() - private var lastActivity: Activity? = null + var lastActivity: Activity? = null private var brandColor: Int? = null override fun onCreateViewHolder( From fc99a409a0c833aa5180916e62a73a4f2a4b6420 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 21 Oct 2026 09:49:08 +0200 Subject: [PATCH 49/96] Unnecessary lateinit Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesFragment.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index bb2107b8b..cb189b41d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -40,7 +40,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie lateinit var binding : FragmentActivitiesBinding private lateinit var note: Note private lateinit var account: Account - private lateinit var adapter: ActivityListAdapter + private val adapter: ActivityListAdapter = ActivityListAdapter(this) private lateinit var activityDataProvider: ActivitiesDataProvider @@ -74,9 +74,6 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie id, this ) - adapter = ActivityListAdapter( - this - ) binding.list.adapter = adapter binding.swipeContainingList.setOnRefreshListener { // Reload list From c16f12ea1e4c10ab40f1a1c8bb8872fbeea2f613 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 10:47:56 +0200 Subject: [PATCH 50/96] Edge case fix + restored missing error message Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 2 +- .../owncloud/notes/activities/ActivitiesFragment.kt | 9 +++++---- app/src/main/res/values/strings.xml | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index 909494bc3..ac608645d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -45,7 +45,7 @@ class ActivitiesDataProvider ( when { activitiesResult is ActivitiesResult.Success && versionsResult is VersionsResult.Success -> { - data.activitiesLastGiven = activitiesResult.lastGiven + data.activitiesLastGiven = activitiesResult.lastGiven ?: data.activitiesLastGiven data.activities.addAll(activitiesResult.activities) data.versions.addAll(versionsResult.versions) listener.onPageFetched() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index cb189b41d..e7a16a928 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -104,10 +104,11 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie val result = repository.restoreVersion(version) withContext(Dispatchers.Main) { val message = - if (result == RestoreVersionsResult.Success) - R.string.version_restored - else - R.string.version_restore_failed + when (result) { + RestoreVersionsResult.Success -> R.string.version_restored + RestoreVersionsResult.FailedUnsupported -> R.string.version_restore_failed_unsupported + else -> R.string.version_restore_failed + } BrandedSnackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1a90f237..d76f4068b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -551,7 +551,6 @@ Activities Unable to fetch activities - No activity present for this note Activity No events like additions, changes and shares yet. An error occurred loading activities From f26198a21e61648abfa535b847c2c3e36dec3bae Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 11:17:44 +0200 Subject: [PATCH 51/96] Prevent empty calls to activities Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 9 ++++++++- .../owncloud/notes/activities/adapter/ActivitiesData.kt | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index ac608645d..45d5a01ee 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -35,7 +35,10 @@ class ActivitiesDataProvider ( job?.cancel() job = lifecycleScope.launch { val (activitiesResult, versionsResult) = withContext(Dispatchers.IO) { - val activities = async { activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) } + val activities = async { + if (data.activitiesMore) activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) + else ActivitiesResult.Success(listOf(), data.activitiesLastGiven) + } val versions = async { if (fetchVersions) versionsRepo.fetchVersions(noteId) else VersionsResult.Success(listOf()) @@ -45,6 +48,9 @@ class ActivitiesDataProvider ( when { activitiesResult is ActivitiesResult.Success && versionsResult is VersionsResult.Success -> { + if (activitiesResult.lastGiven != null && activitiesResult.lastGiven == data.activitiesLastGiven) + data.activitiesMore = false + data.activitiesLastGiven = activitiesResult.lastGiven ?: data.activitiesLastGiven data.activities.addAll(activitiesResult.activities) data.versions.addAll(versionsResult.versions) @@ -65,6 +71,7 @@ class ActivitiesDataProvider ( data.activities.clear() data.versions.clear() data.activitiesLastGiven = null + data.activitiesMore = true } interface OnPageFetchedListener { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index 4c05f49c5..161949f42 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -8,6 +8,7 @@ class ActivitiesData { val activities = arrayListOf() val versions = arrayListOf() var activitiesLastGiven: Long? = null + var activitiesMore: Boolean = true fun getAggregatedList(context: Context) : List { val items = arrayListOf() From 5a4f3bae2586d96df64df226a86a1ddba9f6f14b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 16:25:44 +0200 Subject: [PATCH 52/96] Fixed empty list on servers with no Activities app Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 4 +++- .../notes/activities/repository/ActivitiesRepository.kt | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index 45d5a01ee..f46e0e2fe 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -32,11 +32,13 @@ class ActivitiesDataProvider ( fun fetchNextPage() { // Versions are non-paginated: fetch only once val fetchVersions = data.isEmpty() + val fetchActivities = data.activitiesMore + job?.cancel() job = lifecycleScope.launch { val (activitiesResult, versionsResult) = withContext(Dispatchers.IO) { val activities = async { - if (data.activitiesMore) activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) + if (fetchActivities) activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) else ActivitiesResult.Success(listOf(), data.activitiesLastGiven) } val versions = async { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index c3d8b6c85..05b7e60b1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount import it.niedermann.owncloud.notes.persistence.ApiProvider import java.net.HttpURLConnection +import kotlin.getValue class ActivitiesRepository( private val context: Context, @@ -22,6 +23,11 @@ class ActivitiesRepository( ) } + // Activity capability not present + if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { + return ActivitiesResult.Success(listOf(), null) + } + return ActivitiesResult.Failed() } From 5dc5ecb63ccd55cd975a7b05d0a9ab844ce13a09 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 16:49:46 +0200 Subject: [PATCH 53/96] Dates in list Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 5 +++-- .../activities/adapter/ActivityListAdapter.kt | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index e7a16a928..8d38b958f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -40,11 +40,12 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie lateinit var binding : FragmentActivitiesBinding private lateinit var note: Note private lateinit var account: Account - private val adapter: ActivityListAdapter = ActivityListAdapter(this) + private lateinit var adapter: ActivityListAdapter private lateinit var activityDataProvider: ActivitiesDataProvider - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + adapter = ActivityListAdapter(requireContext(), this) binding = FragmentActivitiesBinding.inflate(inflater, container, false) initializeArguments() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 6fe17470e..78477b68a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.nextcloud.android.common.core.utils.DateFormatter import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R @@ -17,14 +18,18 @@ import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandedViewHolder import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding +import java.time.format.DateTimeFormatter +import java.util.Calendar class ActivityListAdapter ( + private val context: Context, private val listener: OnActivityClickListener ) : RecyclerView.Adapter(), Branded { val items = arrayListOf() var lastActivity: Activity? = null private var brandColor: Int? = null + private val dateFormatter = DateFormatter(context) override fun onCreateViewHolder( parent: ViewGroup, @@ -35,7 +40,8 @@ class ActivityListAdapter ( LayoutInflater.from(parent.context), parent, false - ) + ), + dateFormatter ) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { @@ -61,7 +67,7 @@ class ActivityListAdapter ( private class ActivityViewHolder( val binding: ActivityListItemBinding, - //val brandingUtil: BrandingUtil + val dateFormatter: DateFormatter ) : BrandedViewHolder(binding.root) { private var brandingUtil: BrandingUtil? = null @@ -83,7 +89,11 @@ class ActivityListAdapter ( private fun bindDateTime(activity: ActivityListItem) { binding.datetime.apply { visibility = View.VISIBLE - text = DateFormat.format(TIME_PATTERN, activity.getTimestamp()) + text = dateFormatter.getConditionallyRelativeFormattedTimeSpan( + Calendar.getInstance().apply { + timeInMillis = activity.getTimestamp() + } + ) } } @@ -136,7 +146,6 @@ class ActivityListAdapter ( } companion object { - private const val TIME_PATTERN = "HH:mm" private const val COLORED_ICON_SUFFIX = "-color.svg" } From ed72d57869501cbaf4f4409fb954e15b5fafc5b7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 17:56:01 +0200 Subject: [PATCH 54/96] Cleaner repo impl, fixed load loop Signed-off-by: daniele-verducci --- .../activities/ActivitiesDataProvider.kt | 72 +++++++++++++------ .../notes/activities/ActivitiesFragment.kt | 6 +- .../repository/ActivitiesRepository.kt | 2 +- .../activities/repository/ActivitiesResult.kt | 2 + 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index f46e0e2fe..f929d347b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -29,43 +29,71 @@ class ActivitiesDataProvider ( private val activitiesRepo = ActivitiesRepository(context, ssoAccount) private val versionsRepo = VersionsRepository(context, ssoAccount) - fun fetchNextPage() { - // Versions are non-paginated: fetch only once - val fetchVersions = data.isEmpty() - val fetchActivities = data.activitiesMore - + fun fetchFirstPage() { job?.cancel() job = lifecycleScope.launch { val (activitiesResult, versionsResult) = withContext(Dispatchers.IO) { val activities = async { - if (fetchActivities) activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) - else ActivitiesResult.Success(listOf(), data.activitiesLastGiven) + activitiesRepo.fetchActivities(noteId) } val versions = async { - if (fetchVersions) versionsRepo.fetchVersions(noteId) - else VersionsResult.Success(listOf()) + versionsRepo.fetchVersions(noteId) } activities.await() to versions.await() } - when { - activitiesResult is ActivitiesResult.Success && versionsResult is VersionsResult.Success -> { - if (activitiesResult.lastGiven != null && activitiesResult.lastGiven == data.activitiesLastGiven) - data.activitiesMore = false + if (!parseActivitiesResult(activitiesResult)) return@launch - data.activitiesLastGiven = activitiesResult.lastGiven ?: data.activitiesLastGiven - data.activities.addAll(activitiesResult.activities) - data.versions.addAll(versionsResult.versions) - listener.onPageFetched() - return@launch - } + if (!parseVersionsResult(versionsResult)) return@launch - activitiesResult is ActivitiesResult.Failed -> Log.e(TAG, "Fetching activities failed") + listener.onPageFetched() + } + } - versionsResult is VersionsResult.Failed -> Log.e(TAG, "Fetching versions failed") + fun fetchNextPage() { + job?.cancel() + job = lifecycleScope.launch { + val activitiesResult = activitiesRepo.fetchActivities(noteId) + + 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 } + } + } - listener.onPageFetchError() + 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 + } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 8d38b958f..aa46b2555 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -79,10 +79,10 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie binding.swipeContainingList.setOnRefreshListener { // Reload list activityDataProvider.reset() - activityDataProvider.fetchNextPage() + activityDataProvider.fetchFirstPage() } - activityDataProvider.fetchNextPage() + activityDataProvider.fetchFirstPage() } override fun applyBrand(color: Int) { @@ -116,7 +116,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie // Reload list if (result == RestoreVersionsResult.Success) { activityDataProvider.reset() - activityDataProvider.fetchNextPage() + activityDataProvider.fetchFirstPage() } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 05b7e60b1..fcf2bfdf2 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -25,7 +25,7 @@ class ActivitiesRepository( // Activity capability not present if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { - return ActivitiesResult.Success(listOf(), null) + return ActivitiesResult.FailedNoCapability } return ActivitiesResult.Failed() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt index 7fe60a31d..4bae13973 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -7,6 +7,8 @@ sealed class ActivitiesResult(open val descriptionId: Int? = null) { data class Success(val activities: List, val lastGiven: Long?) : ActivitiesResult() + data object FailedNoCapability : ActivitiesResult() + data class Failed( override val descriptionId: Int = R.string.activities_fetch_failed ) : ActivitiesResult(descriptionId) From 5435c6d94135abbce93babbf0650daede5f350bf Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 18:02:43 +0200 Subject: [PATCH 55/96] Catch exception Signed-off-by: daniele-verducci --- .../repository/ActivitiesRepository.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index fcf2bfdf2..e6d6a5b7c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -1,8 +1,10 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context +import android.util.Log import com.nextcloud.android.sso.model.SingleSignOnAccount import it.niedermann.owncloud.notes.persistence.ApiProvider +import okio.IOException import java.net.HttpURLConnection import kotlin.getValue @@ -14,24 +16,29 @@ class ActivitiesRepository( private val api by lazy { ApiProvider.getInstance().getActivitiesAPI(context, ssoAccount) } fun fetchActivities(noteId: Long, since: Long? = null): ActivitiesResult { - val response = api.fetchActivities(noteId, since, PAGE_SIZE).execute() - - if (response.isSuccessful || response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { - return ActivitiesResult.Success( - response.body()?.ocs?.data ?: listOf(), - response.headers()[HEADER_LAST_GIVEN]?.toLongOrNull() - ) - } - - // Activity capability not present - if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { - return ActivitiesResult.FailedNoCapability + try { + val response = api.fetchActivities(noteId, since, PAGE_SIZE).execute() + + if (response.isSuccessful || response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return ActivitiesResult.Success( + response.body()?.ocs?.data ?: listOf(), + response.headers()[HEADER_LAST_GIVEN]?.toLongOrNull() + ) + } + + // Activity capability not present + if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { + return ActivitiesResult.FailedNoCapability + } + } catch (e: IOException) { + Log.e(TAG, "Unable to fetch activities: $e") } return ActivitiesResult.Failed() } companion object { + val TAG = ActivitiesRepository::class.java.simpleName const val PAGE_SIZE = 50 private const val HEADER_LAST_GIVEN = "X-Activity-Last-Given" } From 0cd246adef2386648ed73d7b5d7f3137fc010ef3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 18:09:05 +0200 Subject: [PATCH 56/96] Fix wrong page loaded Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index f929d347b..2524ff28c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -51,9 +51,14 @@ class ActivitiesDataProvider ( } fun fetchNextPage() { - job?.cancel() + if (job?.isActive == true) return + + if (!data.activitiesMore) return + job = lifecycleScope.launch { - val activitiesResult = activitiesRepo.fetchActivities(noteId) + val activitiesResult = withContext(Dispatchers.IO) { + activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) + } if (!parseActivitiesResult(activitiesResult)) return@launch From 4816f83f610fb7a9a754f1f1d83f2ce4a7e237f8 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 22 Oct 2026 18:44:13 +0200 Subject: [PATCH 57/96] Close req Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 2 +- .../niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index 2524ff28c..2cfb04cd9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -54,7 +54,7 @@ class ActivitiesDataProvider ( if (job?.isActive == true) return if (!data.activitiesMore) return - + job = lifecycleScope.launch { val activitiesResult = withContext(Dispatchers.IO) { activitiesRepo.fetchActivities(noteId, data.activitiesLastGiven) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index 9adbd9603..b69de8312 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -48,7 +48,7 @@ class VersionsAPI ( return try { val response = nextcloudAPI.performNetworkRequestV2(request) - VersionsResult.Success(readData(response, fileId)) + response.body.use { VersionsResult.Success(readData(response, fileId)) } } catch (e: Exception) { Log.e(TAG, "Unable to fetch versions: $e") VersionsResult.Failed @@ -70,6 +70,7 @@ class VersionsAPI ( try { val response = nextcloudAPI.performNetworkRequestV2(request) Log.i(TAG, response.toString()) + response.body.close() return RestoreVersionsResult.Success } catch (e: NextcloudUnsupportedMethodException) { Log.e(TAG, "restoreVersion used an unsupported method: ${e.message}") From 58ff913b6d1f3c7b2fa1b39d86445cf822aff1f3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 09:23:01 +0200 Subject: [PATCH 58/96] 3-dots button in list alighed to Files app Signed-off-by: daniele-verducci --- .../layout/item_notes_list_note_item_grid.xml | 30 +++++++++---------- ...m_notes_list_note_item_grid_only_title.xml | 30 +++++++++---------- ...item_notes_list_note_item_with_excerpt.xml | 24 +++++++-------- app/src/main/res/values/dimens.xml | 2 -- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml index 21425b0a6..94cd41a3a 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml @@ -87,8 +87,9 @@ - - + + diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml index 23239131e..9de193b11 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml @@ -76,8 +76,9 @@ - - + + diff --git a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml index 995a859eb..65e857cf0 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml @@ -168,22 +168,20 @@ tools:maxLength="15" tools:text="@tools:sample/lorem/random" /> - - + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 4379bc06f..2c3f15ad7 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -26,8 +26,6 @@ 180dp 100dp - 18dp - 40dp 16dp From 15101d6795d1be1e1dfa9a5c40cfae7b47b963e9 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 10:36:12 +0200 Subject: [PATCH 59/96] Added empty bottom sheet Signed-off-by: daniele-verducci --- .../notes/itemdetails/ItemDetailsActivity.kt | 22 ++++++++++++-- .../owncloud/notes/main/MainActivity.java | 7 +++-- .../notes/main/items/ItemActionBottomSheet.kt | 29 +++++++++++++++++++ .../notes/main/items/NoteViewHolder.java | 2 +- .../notes/shared/model/NoteClickListener.java | 2 +- .../res/layout/item_action_bottom_sheet.xml | 24 +++++++++++++++ 6 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt create mode 100644 app/src/main/res/layout/item_action_bottom_sheet.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index e37fe4a04..5aeccc0a7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -31,6 +31,7 @@ class ItemDetailsActivity : BrandedActivity() { lateinit var binding : ActivityItemDetailsBinding lateinit var note: Note lateinit var account: Account + lateinit var page: PAGE override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -51,6 +52,8 @@ class ItemDetailsActivity : BrandedActivity() { ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") + page = intent.extras.getSerializableArgument(ARG_PAGE, PAGE::class.java) + ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") } private fun loadHeaderData() { @@ -71,18 +74,24 @@ class ItemDetailsActivity : BrandedActivity() { private fun loadFragments() { val pages = listOf( Page( + PAGE.ACTIVITIES, getString(R.string.activities_title), AppCompatResources.getDrawable(this, R.drawable.nav_activity), ActivitiesFragment.newInstance(note, account) ), Page( + PAGE.SHARE, getString(R.string.share), AppCompatResources.getDrawable(this, R.drawable.ic_share_white_24dp), NoteShareFragment.newInstance(note, account) ) ) - binding.pager.adapter = PagerAdapter(pages) + + // Select page + binding.pager.currentItem = pages.indexOf(pages.find { it.page == page }) + + // Set tabs TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> tab.text = pages[position].title tab.icon = pages[position].icon @@ -98,6 +107,7 @@ class ItemDetailsActivity : BrandedActivity() { } private data class Page ( + val page: PAGE, val title: String, val icon: Drawable?, val fragment: BrandedFragment @@ -110,15 +120,23 @@ class ItemDetailsActivity : BrandedActivity() { override fun createFragment(position: Int) = fragments[position].fragment } + public enum class PAGE { ACTIVITIES, SHARE } + companion object { const val ARG_NOTE: String = "note" const val ARG_ACCOUNT: String = "account" + const val ARG_PAGE: String = "page" + + @JvmStatic + fun getActivityStartIntent(context: Context, note: Note, account: Account): Intent = + getActivityStartIntent(context, note, account, PAGE.ACTIVITIES) @JvmStatic - fun getActivityStartIntent(context: Context, note: Note, account: Account): Intent { + fun getActivityStartIntent(context: Context, note: Note, account: Account, page: PAGE): Intent { val bundle = Bundle() bundle.putSerializable(ARG_NOTE, note) bundle.putSerializable(ARG_ACCOUNT, account) + bundle.putSerializable(ARG_PAGE, page) val intent = Intent(context, ItemDetailsActivity::class.java) intent.putExtras(bundle) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index c25ddcf1f..8bff688cb 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -102,6 +102,7 @@ import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; +import it.niedermann.owncloud.notes.main.items.ItemActionBottomSheet; import it.niedermann.owncloud.notes.main.items.ItemAdapter; import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper; @@ -916,11 +917,11 @@ public void onNoteFavoriteClick(int position, View view) { } @Override - public void openDetails(int position) { + public void openInfoBottomSheet(int position) { if (adapter.getItem(position) instanceof Note note) { mainViewModel.getCurrentAccount().observe(this, (account) -> { - final Intent intent = ItemDetailsActivity.getActivityStartIntent(this, note, account); - startActivity(intent); + final var fragment = new ItemActionBottomSheet(note, account); + fragment.show(getSupportFragmentManager(), ItemActionBottomSheet.TAG); }); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt new file mode 100644 index 000000000..bac722dbd --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt @@ -0,0 +1,29 @@ +package it.niedermann.owncloud.notes.main.items + +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity +import it.niedermann.owncloud.notes.persistence.entity.Account +import it.niedermann.owncloud.notes.persistence.entity.Note + +class ItemActionBottomSheet( + val note: Note, + val account: Account +) : BrandedBottomSheetDialogFragment(R.layout.item_action_bottom_sheet) { + + override fun applyBrand(color: Int) { + // TODO + } + + private fun openInfo(page: ItemDetailsActivity.PAGE) { + val context = context ?: return + val intent = ItemDetailsActivity.getActivityStartIntent(context, note, account, page); + startActivity(intent); + } + + companion object { + @JvmField + val TAG: String = ItemActionBottomSheet::class.simpleName ?: "ItemActionBottomSheet" + } + +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java index d68ac162a..6a93fb581 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java @@ -115,7 +115,7 @@ protected void bindSearchableContent(@NonNull Context context, @NonNull TextView } protected void bindNoteDetailsIcon(@NonNull Context context, @NonNull MaterialButton noteDetailsIcon, int color) { - noteDetailsIcon.setOnClickListener(view -> noteClickListener.openDetails(getLayoutPosition())); + noteDetailsIcon.setOnClickListener(view -> noteClickListener.openInfoBottomSheet(getLayoutPosition())); final var util = BrandingUtil.of(color, context); util.material.colorMaterialButtonContent(noteDetailsIcon, ColorRole.SECONDARY); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java index 5e24d0303..feca07cd4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java @@ -13,5 +13,5 @@ public interface NoteClickListener { void onNoteFavoriteClick(int position, View v); - void openDetails(int position); + void openInfoBottomSheet(int position); } \ No newline at end of file diff --git a/app/src/main/res/layout/item_action_bottom_sheet.xml b/app/src/main/res/layout/item_action_bottom_sheet.xml new file mode 100644 index 000000000..ca4bd643a --- /dev/null +++ b/app/src/main/res/layout/item_action_bottom_sheet.xml @@ -0,0 +1,24 @@ + + + + + + From bc18d1270940be7c7fa58aeb9282d8454ffc0aea Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 11:00:17 +0200 Subject: [PATCH 60/96] Init bottom sheet layout Signed-off-by: daniele-verducci --- .../notes/main/items/ItemActionBottomSheet.kt | 30 ++++++++++++++- app/src/main/res/drawable/file_text.xml | 17 +++++++++ .../res/layout/item_action_bottom_sheet.xml | 38 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/file_text.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt index bac722dbd..e009d9dd3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt @@ -1,7 +1,12 @@ package it.niedermann.owncloud.notes.main.items +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment +import it.niedermann.owncloud.notes.databinding.ItemActionBottomSheetBinding import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity import it.niedermann.owncloud.notes.persistence.entity.Account import it.niedermann.owncloud.notes.persistence.entity.Note @@ -11,13 +16,36 @@ class ItemActionBottomSheet( val account: Account ) : BrandedBottomSheetDialogFragment(R.layout.item_action_bottom_sheet) { + lateinit var binding: ItemActionBottomSheetBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ItemActionBottomSheetBinding.inflate(layoutInflater, container, false) + + setTitle() + addActions() + + return binding.root + } + + fun setTitle() { + binding.title.text = note.title + } + + fun addActions() { + + } + override fun applyBrand(color: Int) { // TODO } private fun openInfo(page: ItemDetailsActivity.PAGE) { val context = context ?: return - val intent = ItemDetailsActivity.getActivityStartIntent(context, note, account, page); + val intent = ItemDetailsActivity.getActivityStartIntent(context, note, account, page) startActivity(intent); } diff --git a/app/src/main/res/drawable/file_text.xml b/app/src/main/res/drawable/file_text.xml new file mode 100644 index 000000000..9ec2d79bb --- /dev/null +++ b/app/src/main/res/drawable/file_text.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/layout/item_action_bottom_sheet.xml b/app/src/main/res/layout/item_action_bottom_sheet.xml index ca4bd643a..6daf4abbb 100644 --- a/app/src/main/res/layout/item_action_bottom_sheet.xml +++ b/app/src/main/res/layout/item_action_bottom_sheet.xml @@ -21,4 +21,42 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + + + + + + + + + + + + + From 902e3c767d9d91a544ae3e6b19fa762a4b03019d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 11:04:04 +0200 Subject: [PATCH 61/96] Moved file to its own package Signed-off-by: daniele-verducci --- .../notes/{main/items => itemactions}/ItemActionBottomSheet.kt | 2 +- .../java/it/niedermann/owncloud/notes/main/MainActivity.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/{main/items => itemactions}/ItemActionBottomSheet.kt (96%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt similarity index 96% rename from app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt rename to app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt index e009d9dd3..12ae63b7b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemActionBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt @@ -1,4 +1,4 @@ -package it.niedermann.owncloud.notes.main.items +package it.niedermann.owncloud.notes.itemactions import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 8bff688cb..4b6fd2241 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -101,8 +101,7 @@ import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; -import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; -import it.niedermann.owncloud.notes.main.items.ItemActionBottomSheet; +import it.niedermann.owncloud.notes.itemactions.ItemActionBottomSheet; import it.niedermann.owncloud.notes.main.items.ItemAdapter; import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper; From 3ce18622ed43aca70a7b24102aa27e2125d2a969 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 12:05:48 +0200 Subject: [PATCH 62/96] Completed bottom sheet implementation Signed-off-by: daniele-verducci --- .../itemactions/ItemActionBottomSheet.kt | 51 +++++++++++++++++-- .../owncloud/notes/itemactions/NoteAction.kt | 23 +++++++++ .../res/layout/actions_bottom_sheet_item.xml | 45 ++++++++++++++++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/ids.xml | 12 +++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt create mode 100644 app/src/main/res/layout/actions_bottom_sheet_item.xml create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt index 12ae63b7b..73064bcf4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt @@ -4,19 +4,26 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActionsBottomSheetItemBinding import it.niedermann.owncloud.notes.databinding.ItemActionBottomSheetBinding import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity import it.niedermann.owncloud.notes.persistence.entity.Account import it.niedermann.owncloud.notes.persistence.entity.Note + class ItemActionBottomSheet( val note: Note, val account: Account ) : BrandedBottomSheetDialogFragment(R.layout.item_action_bottom_sheet) { lateinit var binding: ItemActionBottomSheetBinding + private var actionViews : List? = null override fun onCreateView( inflater: LayoutInflater, @@ -26,7 +33,12 @@ class ItemActionBottomSheet( binding = ItemActionBottomSheetBinding.inflate(layoutInflater, container, false) setTitle() - addActions() + addActions( + listOf( + NoteAction.SEE_DETAILS, + NoteAction.SEND_SHARE + ) + ) return binding.root } @@ -35,12 +47,31 @@ class ItemActionBottomSheet( binding.title.text = note.title } - fun addActions() { + fun addActions(actions: List) { + actionViews = actions.map { action -> inflateActionView(action) } + actionViews?.forEach { binding.fileActionsList.addView(it.binding.root) } + } + private fun inflateActionView(action: NoteAction): ActionView { + val itemBinding = ActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false) + .apply { + text.setText(action.title) + icon.setImageResource(action.icon) + root.setOnClickListener { onActionSelected(action) } + } + return ActionView(itemBinding) + } + + fun onActionSelected(action: NoteAction) { + when (action) { + NoteAction.SEE_DETAILS -> openInfo(ItemDetailsActivity.PAGE.ACTIVITIES) + NoteAction.SEND_SHARE -> openInfo(ItemDetailsActivity.PAGE.SHARE) + } + dismiss() } override fun applyBrand(color: Int) { - // TODO + actionViews?.forEach { it.applyBrand(color) } } private fun openInfo(page: ItemDetailsActivity.PAGE) { @@ -49,6 +80,20 @@ class ItemActionBottomSheet( startActivity(intent); } + private data class ActionView( + val binding: ActionsBottomSheetItemBinding + ) : Branded { + + override fun applyBrand(color: Int) { + binding.apply { + val brandingUtil = BrandingUtil.of(color, root.context) + brandingUtil.platform.colorImageView(icon, ColorRole.PRIMARY) + } + } + + } + + companion object { @JvmField val TAG: String = ItemActionBottomSheet::class.simpleName ?: "ItemActionBottomSheet" diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt new file mode 100644 index 000000000..84d27e89a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt @@ -0,0 +1,23 @@ +package it.niedermann.owncloud.notes.itemactions + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import it.niedermann.owncloud.notes.R + +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +enum class NoteAction( + @param:IdRes val id: Int, + @param:StringRes val title: Int, + @param:DrawableRes val icon: Int +) { + SEE_DETAILS(R.id.action_see_details, R.string.action_see_details, R.drawable.ic_info_outline_grey600_24dp), + SEND_SHARE(R.id.action_send_share, R.string.action_send_share, R.drawable.ic_share_white_24dp), +} \ No newline at end of file diff --git a/app/src/main/res/layout/actions_bottom_sheet_item.xml b/app/src/main/res/layout/actions_bottom_sheet_item.xml new file mode 100644 index 000000000..9781486e9 --- /dev/null +++ b/app/src/main/res/layout/actions_bottom_sheet_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2c3f15ad7..a4cfc4795 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -82,6 +82,7 @@ 24dp @dimen/spacer_2x + @dimen/spacer_1x 32dp 32dp diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 000000000..98d624e27 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d76f4068b..a8bf9b3f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -565,5 +565,7 @@ 12:23:45 This is a placeholder + Details + Send/share From 932f0abd866eb9a8a9be2b7e6e932e9959108743 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 12:26:48 +0200 Subject: [PATCH 63/96] Branding, aligned info screen to Files' Signed-off-by: daniele-verducci --- .../activities/adapter/ActivityListAdapter.kt | 21 ++++--------------- .../notes/itemdetails/ItemDetailsActivity.kt | 2 +- .../main/res/layout/activity_item_details.xml | 7 ++++++- app/src/main/res/values/dimens.xml | 2 ++ 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 78477b68a..9e80189d3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -47,9 +47,7 @@ class ActivityListAdapter ( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { // Apply brand val holder = (holder as ActivityViewHolder) - if (brandColor != null) { - holder.applyBrand(brandColor!!) - } + brandColor?.apply { holder.applyBrand(this) } // Retrieve item and bind vh val item = items[position] @@ -70,8 +68,6 @@ class ActivityListAdapter ( val dateFormatter: DateFormatter ) : BrandedViewHolder(binding.root) { - private var brandingUtil: BrandingUtil? = null - fun bind(item: ActivityListItem, listener: OnActivityClickListener) { bindDateTime(item) bindSubject(item) @@ -126,13 +122,6 @@ class ActivityListAdapter ( .placeholder( R.drawable.ic_activity) .error(R.drawable.ic_activity) .into(binding.icon) - - if (iconUrl.endsWith(COLORED_ICON_SUFFIX, ignoreCase = true)) { - binding.icon.imageTintList = null - } else { - brandingUtil?.platform?.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) - } - } private fun bindActionIcon(activity: ActivityListItem) { @@ -142,11 +131,9 @@ class ActivityListAdapter ( } override fun applyBrand(color: Int) { - brandingUtil = BrandingUtil.of(color, binding.root.context) - } - - companion object { - private const val COLORED_ICON_SUFFIX = "-color.svg" + val util = BrandingUtil.of(color, binding.root.context) + util.platform.colorViewBackground(binding.root) + util.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 5aeccc0a7..b43794b66 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -100,7 +100,7 @@ class ItemDetailsActivity : BrandedActivity() { override fun applyBrand(color: Int) { val util = BrandingUtil.of(color, this) - util.material.themeToolbar(binding.toolbar); + util.material.themeToolbar(binding.toolbar) util.platform.colorViewBackground(binding.getRoot()) util.platform.colorViewBackground(binding.tabLayout) util.material.themeTabLayout(binding.tabLayout) diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml index e8e2304fd..77d882ccc 100644 --- a/app/src/main/res/layout/activity_item_details.xml +++ b/app/src/main/res/layout/activity_item_details.xml @@ -11,7 +11,8 @@ android:id="@+id/detail_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + android:background="@color/bg_default"> + 24dp -3dp + 112dp + From 4ab6ff2adc7a0e81d64eec3a25b543983e3aab75 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 12:37:47 +0200 Subject: [PATCH 64/96] Fix crash Signed-off-by: daniele-verducci --- .../owncloud/notes/share/NoteShareFragment.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 38a41f395..a01a99686 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -99,7 +99,14 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar private var account: Account? = null private var repository: ShareRepository? = null private var capabilities: Capabilities? = null - private var resultLauncher: ActivityResultLauncher? = null + private val resultLauncher: ActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == RESULT_OK && it.data != null) { + updateShareeListAdapter() + } + } + private val shares: MutableList = Collections.synchronizedList(ArrayList()) @@ -303,7 +310,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val intent = Intent(context, NoteShareDetailActivity::class.java) intent.putExtras(bundle) - resultLauncher!!.launch(intent) + resultLauncher.launch(intent) } private fun accountOwnsFile(): Boolean { @@ -669,7 +676,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val intent = Intent(context, NoteShareDetailActivity::class.java) intent.putExtras(bundle) - resultLauncher!!.launch(intent) + resultLauncher.launch(intent) } private val expDateShown: Boolean From 4d5fd6428bb4be3ca6eccedb222b23fa5c7036a2 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 12:59:12 +0200 Subject: [PATCH 65/96] Safer context Signed-off-by: daniele-verducci --- .../owncloud/notes/share/NoteShareFragment.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index a01a99686..179eeab85 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -106,7 +106,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar updateShareeListAdapter() } } - + private val shares: MutableList = Collections.synchronizedList(ArrayList()) @@ -128,9 +128,10 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar executorService!!.submit { try { + val context = context ?: return@submit val ssoAcc = - SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) - repository = ShareRepository(requireContext(), ssoAcc) + SingleAccountHelper.getCurrentSingleSignOnAccount(context) + repository = ShareRepository(context, ssoAcc) capabilities = repository!!.getCapabilities() repository!!.fetchSharesForNotesAndSaveShareEntities() @@ -145,11 +146,11 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar account ) ) - binding!!.sharesList.setLayoutManager(LinearLayoutManager(requireContext())) + binding!!.sharesList.setLayoutManager(LinearLayoutManager(context)) binding!!.pickContactEmailBtn.setOnClickListener { checkContactPermission() } binding!!.btnShareButton.setOnClickListener { ShareUtil.openShareDialog( - requireContext(), + context, note!!.title, note!!.content ) @@ -201,8 +202,9 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar } private fun setupSearchView() { - val suggestionAdapter = SuggestionAdapter(requireContext(), null, account!!) - val provider = UsersAndGroupsSearchProvider(requireContext(), repository) + val context = context ?: return + val suggestionAdapter = SuggestionAdapter(context, null, account!!) + val provider = UsersAndGroupsSearchProvider(context, repository) binding!!.searchView.setSuggestionsAdapter(suggestionAdapter) binding!!.searchView.setIconifiedByDefault(false) @@ -341,7 +343,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar getString(R.string.note_share_activity_shared_with_you), share.ownerDisplayName ) - AvatarLoader.load(requireContext(), binding!!.sharedWithYouAvatar, account!!) + context?.apply { AvatarLoader.load(this, binding!!.sharedWithYouAvatar, account!!) } binding!!.sharedWithYouAvatar.setVisibility(View.VISIBLE) val description = share.note @@ -433,12 +435,12 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar if (TextUtils.isEmpty(share.shareLink)) { copyAndShareFileLink(share.shareLink) } else { - copyToClipboard(requireContext(), share.shareLink) + context?.apply { copyToClipboard(this, share.shareLink) } } } private fun copyAndShareFileLink(link: String?) { - copyToClipboard(requireContext(), link, false) + context?.apply { copyToClipboard(this, link, false) } val snackbar = Snackbar .make( requireView(), @@ -463,8 +465,10 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val snackbar = Snackbar .make(binding!!.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) .setAction(R.string.dismiss) { } - val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(requireContext()), requireContext()) - util.material.themeSnackbar(snackbar) + context?.apply { + val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this) + util.material.themeSnackbar(snackbar) + } snackbar.show() } @@ -542,8 +546,9 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar } private fun checkContactPermission() { + val context = context ?: return if (ContextCompat.checkSelfPermission( - requireContext(), + context, Manifest.permission.READ_CONTACTS ) == PackageManager.PERMISSION_GRANTED ) { @@ -554,10 +559,11 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar } private fun pickContactEmail() { + val context = context ?: return val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) // FIXME: - if (intent.resolveActivity(requireContext().packageManager) != null) { + if (intent.resolveActivity(context.packageManager) != null) { onContactSelectionResultLauncher.launch(intent) } else { DisplayUtils.showSnackMessage( @@ -795,7 +801,8 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar }) override fun applyBrand(color: Int) { - val util = BrandingUtil.of(color, requireContext()) + val context = context ?: return + val util = BrandingUtil.of(color, context) util.notes.themeContentSearchView(binding!!.searchView) util.platform.colorImageView(binding!!.searchViewIcon, ColorRole.ON_SURFACE_VARIANT) util.platform.colorImageView(binding!!.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT) From 1c5e028397dd91ed62b810bab8135bfd6298572e Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 14:14:15 +0200 Subject: [PATCH 66/96] Added missing headers Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 7 +++++++ .../owncloud/notes/activities/adapter/ActivitiesData.kt | 7 +++++++ .../notes/activities/adapter/ActivityListAdapter.kt | 7 +++++++ .../owncloud/notes/activities/adapter/ActivityListItem.kt | 7 +++++++ .../notes/activities/adapter/OnActivityClickListener.kt | 7 +++++++ .../notes/activities/repository/ActivitiesRepository.kt | 7 +++++++ .../notes/activities/repository/ActivitiesResult.kt | 7 +++++++ .../notes/activities/repository/RestoreVersionsResult.kt | 7 +++++++ .../notes/activities/repository/VersionsRepository.kt | 7 +++++++ .../owncloud/notes/activities/repository/VersionsResult.kt | 7 +++++++ .../owncloud/notes/itemactions/ItemActionBottomSheet.kt | 7 +++++++ 11 files changed, 77 insertions(+) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index 2cfb04cd9..c2b06f0bd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * 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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index 161949f42..678dd9aa4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * 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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 9e80189d3..a480e8ef9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * 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.annotation.SuppressLint diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt index c1f6811a4..be0eae557 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * 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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt index 3682fb505..0893426ef 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.adapter import com.owncloud.android.lib.resources.files.model.FileVersion diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index e6d6a5b7c..3d631112e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.repository import android.content.Context diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt index 4bae13973..34a6e8632 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.repository import it.niedermann.owncloud.notes.R diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt index f4b8b236d..b9844efa0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.repository import com.owncloud.android.lib.resources.files.model.FileVersion diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index 5a17df93e..1d3203bbc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.repository import android.content.Context diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt index 2ecf814e4..96f4b167e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.repository import com.owncloud.android.lib.resources.files.model.FileVersion diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt index 73064bcf4..4cb7a49ab 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/ItemActionBottomSheet.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.itemactions import android.os.Bundle From eecd4bee2cbc032016d634ba8fc14eeddccd35a6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 14:55:13 +0200 Subject: [PATCH 67/96] Format file sizes Signed-off-by: daniele-verducci --- .../activities/adapter/ActivityListAdapter.kt | 4 -- .../activities/adapter/ActivityListItem.kt | 3 +- .../notes/util/HumanReadableFormatter.kt | 42 +++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/HumanReadableFormatter.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index a480e8ef9..0a86fdfbf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -7,9 +7,7 @@ */ 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 @@ -18,14 +16,12 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.nextcloud.android.common.core.utils.DateFormatter import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.model.Activity import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandedViewHolder import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding -import java.time.format.DateTimeFormatter import java.util.Calendar class ActivityListAdapter ( diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt index be0eae557..825fbdac5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt @@ -11,6 +11,7 @@ import android.content.Context import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.util.HumanReadableFormatter class ActivityListItem : Comparable { val context: Context @@ -46,7 +47,7 @@ class ActivityListItem : Comparable { fun getMessage() : String? = when { activity != null -> activity.message - fileVersion != null -> "${fileVersion.fileLength} B" + fileVersion != null -> HumanReadableFormatter.formatBytes(fileVersion.fileLength) else -> null } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/HumanReadableFormatter.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/HumanReadableFormatter.kt new file mode 100644 index 000000000..8224c3da1 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/HumanReadableFormatter.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package it.niedermann.owncloud.notes.util + +import java.util.Locale + +object HumanReadableFormatter { + + private const val BITS_PER_UNIT = 10 + + @JvmStatic + fun formatBytes(bytes: Long): String? { + if (bytes < 0) { + return null + } + + val highestSetBit = Long.SIZE_BITS - 1 - bytes.countLeadingZeroBits() + val unitIndex = (highestSetBit / BITS_PER_UNIT).coerceIn(0, ByteUnit.entries.lastIndex) + val unit = ByteUnit.entries[unitIndex] + val size = bytes.toDouble() / (1L shl (BITS_PER_UNIT * unitIndex)) + + val pattern = "%.${unit.decimals}f" + val formattedSize = String.format(Locale.ROOT, pattern, size) + return "$formattedSize ${unit.suffix}" + } + + enum class ByteUnit(val suffix: String, val decimals: Int) { + BYTES("B", 0), + KILOBYTES("KB", 0), + MEGABYTES("MB", 1), + GIGABYTES("GB", 1), + TERABYTES("TB", 1), + PETABYTES("PB", 2), + EXABYTES("EB", 2) + } + +} From 59012909040b0f90a2502cbe8ffa6bef1ede4959 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 15:00:02 +0200 Subject: [PATCH 68/96] Fix Signed-off-by: daniele-verducci --- .../niedermann/owncloud/notes/main/MainActivity.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 4b6fd2241..6d8bbc75d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -918,10 +918,12 @@ public void onNoteFavoriteClick(int position, View view) { @Override public void openInfoBottomSheet(int position) { if (adapter.getItem(position) instanceof Note note) { - mainViewModel.getCurrentAccount().observe(this, (account) -> { - final var fragment = new ItemActionBottomSheet(note, account); - fragment.show(getSupportFragmentManager(), ItemActionBottomSheet.TAG); - }); + Account account = mainViewModel.getCurrentAccount().getValue(); + if (account == null) return; + + final var fragment = new ItemActionBottomSheet(note, account); + fragment.show(getSupportFragmentManager(), ItemActionBottomSheet.TAG); + } } From 5d5f7a0813bdfeac8d6abccbd96ec14d1751c648 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 15:02:21 +0200 Subject: [PATCH 69/96] Hide info button when more than one element is selected Signed-off-by: daniele-verducci --- .../java/it/niedermann/owncloud/notes/main/MainActivity.java | 1 + .../owncloud/notes/main/MultiSelectedActionModeCallback.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 6d8bbc75d..0c65ce160 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -636,6 +636,7 @@ public void onSelectionChanged() { if (tracker.hasSelection()) { int selected = tracker.getSelection().size(); mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, selected, selected)); + mActionMode.invalidate(); } else { mActionMode.finish(); mActionMode = null; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 0e49e6970..470d3e7e1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -102,8 +102,8 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // Info menu makes sense only for single selection - menu.findItem(R.id.menu_details).setEnabled(tracker.getSelection().size() == 1); - return false; + menu.findItem(R.id.menu_details).setVisible(tracker.getSelection().size() == 1); + return true; } /** From 132d4d00319183ee2cc278d90676d18379205fc7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 23 Oct 2026 17:05:18 +0200 Subject: [PATCH 70/96] Sync note after version restore and notify editor show updated note when restoring from editor Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 41 +++++++++++++------ .../owncloud/notes/edit/BaseNoteFragment.java | 4 +- .../owncloud/notes/edit/EditNoteActivity.java | 19 +++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index aa46b2555..cf05bc3ba 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -6,6 +6,7 @@ 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 @@ -21,6 +22,7 @@ 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 @@ -97,6 +99,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie } override fun onVersionItemClick(version: FileVersion) { + binding.swipeContainingList.isRefreshing = true // Restore the version val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) @@ -104,24 +107,36 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie lifecycleScope.launch(Dispatchers.IO) { val result = repository.restoreVersion(version) withContext(Dispatchers.Main) { - val message = - when (result) { - RestoreVersionsResult.Success -> R.string.version_restored - RestoreVersionsResult.FailedUnsupported -> R.string.version_restore_failed_unsupported - else -> R.string.version_restore_failed - } - - BrandedSnackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() - - // Reload list - if (result == RestoreVersionsResult.Success) { - activityDataProvider.reset() - activityDataProvider.fetchFirstPage() + 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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index 201ded2ec..d1e701b3f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -307,8 +307,8 @@ public boolean onOptionsItemSelected(MenuItem item) { .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; } else if (itemId == R.id.menu_details) { - Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(requireContext(), note, localAccount); - startActivity(detailsIntent); + if (getActivity() instanceof EditNoteActivity) + ((EditNoteActivity)getActivity()).openDetails(note, localAccount); return true; } else if (itemId == MENU_ID_PIN) { pinNoteToHome(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java index e94d30973..6dc366466 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java @@ -19,6 +19,8 @@ import android.widget.Toast; import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; @@ -45,6 +47,7 @@ import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.ActivityEditBinding; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.main.MainActivity; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; @@ -71,6 +74,14 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment private BaseNoteFragment fragment; private NotesRepository repo; + private final ActivityResultLauncher detailsLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + refreshExistingNote(getAccountId(), getNoteId()); + } + }); + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -201,6 +212,10 @@ private void launchNoteFragment() { } } + private void refreshExistingNote(long accountId, long noteId) { + launchExistingNote(accountId, noteId, null, true); + } + /** * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note. * The type of fragment (view-mode) is chosen based on the user preferences. @@ -421,6 +436,10 @@ public void close() { } + public void openDetails(Note note, Account localAccount) { + detailsLauncher.launch(ItemDetailsActivity.getActivityStartIntent(this, note, localAccount)); + } + @Override public void onNoteUpdated(Note note) { if (note != null) { From 45438a510272b872da4ff4f40c5da2c622c3f85b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 28 Oct 2026 17:56:26 +0100 Subject: [PATCH 71/96] Avoid creating a Calendar instance for every item Signed-off-by: daniele-verducci --- .../notes/activities/adapter/ActivityListAdapter.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 0a86fdfbf..d26542835 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -34,6 +34,8 @@ class ActivityListAdapter ( private var brandColor: Int? = null private val dateFormatter = DateFormatter(context) + val calendar: Calendar = Calendar.getInstance() + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -44,7 +46,8 @@ class ActivityListAdapter ( parent, false ), - dateFormatter + dateFormatter, + calendar ) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { @@ -68,7 +71,8 @@ class ActivityListAdapter ( private class ActivityViewHolder( val binding: ActivityListItemBinding, - val dateFormatter: DateFormatter + val dateFormatter: DateFormatter, + val calendar: Calendar ) : BrandedViewHolder(binding.root) { fun bind(item: ActivityListItem, listener: OnActivityClickListener) { @@ -89,7 +93,7 @@ class ActivityListAdapter ( binding.datetime.apply { visibility = View.VISIBLE text = dateFormatter.getConditionallyRelativeFormattedTimeSpan( - Calendar.getInstance().apply { + calendar.apply { timeInMillis = activity.getTimestamp() } ) From e3ff84935ccf8df01277edad87279ef5f34e018f Mon Sep 17 00:00:00 2001 From: Daniele Verducci Date: Wed, 28 Oct 2026 18:02:40 +0100 Subject: [PATCH 72/96] Cast Signed-off-by: daniele-verducci --- .../it/niedermann/owncloud/notes/edit/BaseNoteFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index d1e701b3f..d8ebc97c8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -307,8 +307,8 @@ public boolean onOptionsItemSelected(MenuItem item) { .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; } else if (itemId == R.id.menu_details) { - if (getActivity() instanceof EditNoteActivity) - ((EditNoteActivity)getActivity()).openDetails(note, localAccount); + if (getActivity() instanceof EditNoteActivity editNoteActivity) + editNoteActivity.openDetails(note, localAccount); return true; } else if (itemId == MENU_ID_PIN) { pinNoteToHome(); From db54c1507c1c19a907eb4843ece3df75d5046f3d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 2 Nov 2026 10:15:05 +0100 Subject: [PATCH 73/96] Sealed class for activities list item Signed-off-by: daniele-verducci --- .../activities/adapter/ActivitiesData.kt | 4 +- .../activities/adapter/ActivityListAdapter.kt | 8 +-- .../activities/adapter/ActivityListItem.kt | 61 ++++++------------- 3 files changed, 26 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index 678dd9aa4..b00b5646a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -20,10 +20,10 @@ class ActivitiesData { fun getAggregatedList(context: Context) : List { val items = arrayListOf() items.addAll( - activities.map { ActivityListItem(context, it) } + activities.map { ActivityListItem.ActivityItem(it) } ) items.addAll( - versions.map { ActivityListItem(context, it) } + versions.map { ActivityListItem.FileVersionItem(context, it) } ) items.sortDescending() return items diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index d26542835..58f9de8d6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -57,7 +57,7 @@ class ActivityListAdapter ( // Retrieve item and bind vh val item = items[position] - if (lastActivity != null && item.activity == lastActivity) { + if (lastActivity != null && (item as? ActivityListItem.ActivityItem)?.activity == lastActivity) { listener.lastActivityReached() } holder.bind(item, listener) @@ -82,9 +82,9 @@ class ActivityListAdapter ( bindIcon(item) bindActionIcon(item) binding.root.setOnClickListener { - when { - item.activity != null -> listener.onActivityItemClick(item.activity) - item.fileVersion != null -> listener.onVersionItemClick(item.fileVersion) + when (item) { + is ActivityListItem.ActivityItem -> listener.onActivityItemClick(item.activity) + is ActivityListItem.FileVersionItem -> listener.onVersionItemClick(item.fileVersion) } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt index 825fbdac5..ea1ebc49f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt @@ -13,48 +13,27 @@ import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.model.Activity import it.niedermann.owncloud.notes.util.HumanReadableFormatter -class ActivityListItem : Comparable { - val context: Context - val activity: Activity? - val fileVersion: FileVersion? +sealed class ActivityListItem : Comparable { + abstract fun getTimestamp(): Long + abstract fun getTitle(): String? + abstract fun getMessage(): String? + open fun getIconUrl(): String? = null + open fun canBeRestored(): Boolean = false - constructor(context: Context, activity: Activity) { - this.context = context - this.activity = activity - this.fileVersion = null - } + override fun compareTo(other: ActivityListItem): Int = + getTimestamp().compareTo(other.getTimestamp()) - constructor(context: Context, fileVersion: FileVersion) { - this.context = context - this.activity = null - this.fileVersion = fileVersion + data class ActivityItem(val activity: Activity) : ActivityListItem() { + override fun getTimestamp(): Long = activity.datetime.time + override fun getTitle(): String = activity.subject + override fun getMessage(): String? = activity.message + override fun getIconUrl(): String? = activity.icon } - fun getTimestamp() : Long = - when { - activity != null -> activity.datetime.time - fileVersion != null -> fileVersion.modifiedTimestamp - else -> 0 - } - - fun getTitle() : String? = - when { - activity != null -> activity.subject - fileVersion != null -> context.getString(R.string.new_version_was_created) - else -> null - } - - fun getMessage() : String? = - when { - activity != null -> activity.message - fileVersion != null -> HumanReadableFormatter.formatBytes(fileVersion.fileLength) - else -> null - } - - fun getIconUrl() : String? = activity?.icon - - fun canBeRestored() : Boolean = fileVersion != null - - override fun compareTo(other: ActivityListItem): Int = - getTimestamp().compareTo(other.getTimestamp()) -} \ No newline at end of file + data class FileVersionItem(val context: Context, val fileVersion: FileVersion) : ActivityListItem() { + override fun getTimestamp(): Long = fileVersion.modifiedTimestamp + override fun getTitle(): String = context.getString(R.string.new_version_was_created) + override fun getMessage(): String = HumanReadableFormatter.formatBytes(fileVersion.fileLength) ?: "" + override fun canBeRestored(): Boolean = true + } +} From 381c22266b2eee153f6b2aed2bf3e812d45b5026 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 2 Nov 2026 10:43:16 +0100 Subject: [PATCH 74/96] Made restore completely asyncronous, fixed spinner showing after failed restore Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index cf05bc3ba..56cc3a3d4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -99,18 +99,18 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie } override fun onVersionItemClick(version: FileVersion) { - binding.swipeContainingList.isRefreshing = true - // Restore the version - val ssoAccount = - SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) - val repository = VersionsRepository(requireContext(), ssoAccount) + 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 -> showMessage(R.string.version_restore_failed_unsupported) - else -> showMessage(R.string.version_restore_failed) + RestoreVersionsResult.FailedUnsupported -> showMessageAndHideProgress(R.string.version_restore_failed_unsupported) + else -> showMessageAndHideProgress(R.string.version_restore_failed) } } } @@ -128,12 +128,15 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie activityDataProvider.fetchFirstPage() // Notify user - showMessage(R.string.version_restored) + showMessageAndHideProgress(R.string.version_restored) }) noteRepo.scheduleSync(account, false) } - private fun showMessage(@StringRes message: Int) { + 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() } @@ -145,7 +148,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie @SuppressLint("NotifyDataSetChanged") override fun onPageFetched() { - binding.swipeContainingList.isRefreshing = false + showProgress(false) binding.emptyList.visibility = View.GONE adapter.items.clear() adapter.items.addAll( @@ -156,7 +159,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie } override fun onPageFetchError() { - binding.swipeContainingList.isRefreshing = false + showProgress(false) if (adapter.items.isEmpty()) binding.emptyList.visibility = View.VISIBLE BrandedSnackbar.make(binding.root, R.string.activities_error, Snackbar.LENGTH_SHORT).show() } From 4f601e6ce9d002a8a42c746b9656606270a6e1a9 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 2 Nov 2026 11:42:01 +0100 Subject: [PATCH 75/96] Update verification-metadata.xml Signed-off-by: daniele-verducci --- gradle/verification-metadata.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8c45513f2..ce165c28e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -113,6 +113,7 @@ + From 1ea8183eab29690df9c2be0334f33e3d969786a7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 2 Nov 2026 18:20:41 +0100 Subject: [PATCH 76/96] Fixed copypasted copyright in a new file Signed-off-by: daniele-verducci --- app/src/main/res/layout/activity_item_details.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml index 77d882ccc..63b1cef6b 100644 --- a/app/src/main/res/layout/activity_item_details.xml +++ b/app/src/main/res/layout/activity_item_details.xml @@ -3,7 +3,7 @@ ~ Nextcloud Notes - Android Client ~ SPDX-FileCopyrightText: 2026 Daniele Verducci ~ SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - ~ SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + ~ SPDX-License-Identifier: GPL-3.0-or-later --> Date: Mon, 2 Nov 2026 18:41:36 +0100 Subject: [PATCH 77/96] Added verification-metadata as imported by Glide's KSP processor Signed-off-by: daniele-verducci --- gradle/verification-metadata.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ce165c28e..dd469b5fe 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -287,6 +287,7 @@ + From a48256c433acdd8c81d4ed003d2baf1e893ea5d8 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 2 Nov 2026 18:47:44 +0100 Subject: [PATCH 78/96] Fixed copypasted copyright in a new file Signed-off-by: daniele-verducci --- app/src/main/res/layout/activity_list_item.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_list_item.xml b/app/src/main/res/layout/activity_list_item.xml index 99448dabb..89f05fcb7 100644 --- a/app/src/main/res/layout/activity_list_item.xml +++ b/app/src/main/res/layout/activity_list_item.xml @@ -3,7 +3,7 @@ ~ Nextcloud - Android Client ~ ~ SPDX-FileCopyrightText: 2016 Andy Scherzinger - ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + ~ SPDX-License-Identifier: AGPL-3.0-or-later --> Date: Tue, 3 Nov 2026 08:35:02 +0300 Subject: [PATCH 79/96] add missing keys Signed-off-by: alperozturk96 --- gradle/verification-keyring.keys | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index def305c44..efe50afdb 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -698,6 +698,63 @@ dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0Pg== =q1C6 -----END PGP PUBLIC KEY BLOCK----- +pub AB2DA4527F6FFC0B +uid Egor Andreevici + +sub 1A94B14C6A03458D +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGI8r9sBEACZJBV2TNUSsLRo89uC4lfmQxfNDqkE0uZghfFY/p0fr6fkBybO +WDkPFskAPD32fzrWxZd2kkyCRyUrOmAUC22q8hw96t28+RqZymvetIa0f8GQGgkO +/ZTiD6Nnv4JoeSfUkJConDk2J/2a0pdxcC/MWLB2I0X6pg4znRHtNjgGsyDe8uW0 +xGK4cyRdQH0A9T0TynKjdB4tBNS6gB7uI4GE+J0jFb56QxzEZ2+t+vaN9QornDgu +yNqZMAp7Fnou1AjmsMalkC9bTO1JrF6P5ndsBPiaLKJHeqHvssC78SGM2CvPjRnS +YVM/pTmcgEPX9590p8WLM050DFMbBMejPx5UDDf461rDcbkatL4XgqopLGDN/+Hp +As7dkGxc5utHm/qrXspaLVpRGaiZ+UiI3m1PJDcOozWXuGSrI2fGw/i0HKY5VHmI +6IVtSrjMu24Fh+t/GK39Li+xZZuw8jZcuL/28kbwVFeeBigz6AKnqvK041iBYfLM +J9y/7B2W1yVr4rvCgBahVvMhJfhnXzNqstWEp+zVLtF3CdNti0eAvJHnoxW8Pu67 +Lx98QyaWHHw+S9sF380O0sQiipldY5X80brrI73MmMnW6bYda6F/57JJ4ae2Hq/Y +Bd9uc8fuInkpBld5uwc698ndl4fB94rm3Z4nFCIv0mKpmEaSo35luiKS6QARAQAB +tCNFZ29yIEFuZHJlZXZpY2kgPGVnb3JAc3F1YXJldXAuY29tPokCUgQTAQgAPBYh +BMWqV/Sjjrp7f5FW3astpFJ/b/wLBQJiPK/bAhsDBQsJCAcCAyICAQYVCgkICwIE +FgIDAQIeBwIXgAAKCRCrLaRSf2/8CwwSD/0RzN1XI+Mj65XgEO7VDZNvKeBLeXJd +Lw1CtCfu71f2KRKhrZ6AaXMSkyv62ZjAcli1bVX5QJW9YFouHBUtKuhi3XsebbET +eSdOggneMr6ZaVB1O1WQYMZFLQmuKeeYkf40BAZTWZLK7+GdPbZ3/c941r0r3bkx +7hg/Kya+3G8/hiYEUxyVf86KJupPlHcrmMHeqirDDhsJJj/WBRZa4sHze1kzz+uE +fyB8DMc+GYbCWnCTh7Fr2ncl2bVlB2EqYgdl5AVh2yTauSPsDM82nw4j2kYm7YvI +0jJ7gdOXAgE1SMUcfsO1WJwwhPDYMIlVgdsGxdxwgCWamaTigxdnJjbOyCMeaH6A +m+t8C6QM+8sER798NVyt6yS8FfvkEspfdPfFqHcqhDhQmRSXbclwT9FCCgSWQjK9 +6VuXsoYPmGDCTnrz9doXZ6lR8YAv5ITFkU/ptCWqLXFGQeyWdecATIxXJfvpVqyd +6OSAgg9JMYKX34iP9CfUpk0LI7H3qIx44LxZK7okxXymo8pKCzy5g39kFkF24wEy +ib8RbTQYJK8+BlJFxGPUHPonMW0OYhWprVQbE5FLj2alvtcjYOA2hlg+KJFW4H2I +vdUsa3r7X39MrtbbfYxUX/QUnysU0uXLerEXFjP4zN/0DfPiaAVC/vzzOQAFZVXE +hqiqR+MiAJ7WmrkCDQRiPK/bARAAu5QxSoH1Yn2McbB6GIuplKt1aIMaZg41meli +sR1EnriOoNnbw3iTI8dx7p7JBIJ2gMCNxu/mct3GcAIaBgj/5Jf9XYVF1bHcazv6 +RPYsvVBYDV1GVaLitG9wlDS/y0wtb3SR7xmhkrwIRJQAjSQtqvRB3lRHguTtatEk +MpTscjgbDjAvzsYx9vtF3jM3dXIK/1rFrC0kOweUZAWJYCNxbdAvJWioas5fKbTK +e4s6KXKhhVVIp/4RIr2dByg5mAK99ZuVyKGhtFE6y0uk+BU4H2ZWXehMPfm9Tjk7 +oqkMC2OqEB1t0Ep9xCQtvzbqCxhNFPuHU+OWTBy7ARnrNKEkh2Bppv607jjHOHxh +JW3sjrl9sH1DAQNR2ZKob70ocUoyqDT4FNG9/H+CjbsZqzFqmKcbAQA2fiIO5NTw +wOnfbcRlmHuY4qrZ5LmhSGnlkrHs9Uld4mosJZXOb69RXIL/d1SCih0wPMBbLl0T +I9FfJD7YDBASxEqN0lmYHGo8qu9Vg5KPSVQW8Fg9Tmig4aPSgpT6nHyqiuUcoZyO +nICX9TMraPXMoXBxXiWrzu8HqtsmzFPpqOmWfvg97X6nco8obpJRGMODUQQMYjeQ +48SBbGVe1utEZ4Yt47ArxLKmh9jp1jdoGkLT+8T0Z1FQEnS1d4/xGPaCFIz4+kXG +QoaJdOkAEQEAAYkCNgQYAQgAIBYhBMWqV/Sjjrp7f5FW3astpFJ/b/wLBQJiPK/b +AhsMAAoJEKstpFJ/b/wLWUkP/25L1N5zJdTsoooTjutFAiVvy8IbXx0XGm0F10pB +MLAbKwkyDyOvZ62DSc/xwmXvZE4GfL+dpvmZUIwmtReYDy4byrMbdF+Yw8xLmnp2 +xeoKsrh8VacVniEkPbKKrVFjBrhoV/oEGHi+ilKq2KeftWHm8mLk/QQ/AXGuum24 +wBmRBBY1NPCiPk8+HoRFY2qrz+cboK5oAp3agCF+LmOBFKUBkId1BxvQZViQGKkm +2uoQ0kiFpy4TlcxWwATtFvaE/d6w/RqdjAwYpZcnbbAh8HthSE1ogjZSWkypEdwr ++EmBlHWaXWxVHsJghB/YVKfC7HS16IalHw8aGxdXsrAU9rnOdajY5NDwdRJDTH5T +M9VsHSIEFOkSr0HBcQ+Ghff9H4QxfeiEo6UsuSZB3ZaRk6VE1GYFRj06mmPB0uv+ +C+6wl5znv6chfQzFPf82KiHO4kxgR+UK3SoxSilCWFh46YXv0IWkg7jeFtJZWLN+ +LGmDS2vF9haurM4dwlz2IOFm7CLSVixCaDPqpOh7OqnwRVHznBy7/DYzmaS0lSAu +TFhh5mq+ofWaIpmdlGiqxuUSkNg/Zj+HBxG+AyH8Lhi9WGUafgJeZ7fKBJbtw6Jp +Wl9bUuiEBpnRFkn12RBe7vkyfZGDR2u/WbxTrhDdrrjoFCFo/ZoYzRY46LR+Hdyt +aEmE +=lnZo +-----END PGP PUBLIC KEY BLOCK----- + pub AC7A514BC9F9BB70 uid Punyashloka Biswal From 867b4bec138c09709f9cfac07ce22297bb649005 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 08:39:10 +0300 Subject: [PATCH 80/96] use files detekt Signed-off-by: alperozturk96 --- detekt.yml | 406 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 detekt.yml diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 000000000..5712ca319 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,406 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +processors: + active: true + exclude: + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: + # - 'ProjectStatisticsReport' + # - 'ComplexityReport' + # - 'NotificationReport' + # - 'FindingsReport' + # - 'BuildFailureReport' + +comments: + active: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + excludes: ['**/androidTest/**'] + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 120 + excludes: ['**/androidTest/**'] + LongParameterList: + active: false + functionThreshold: 7 + constructorThreshold: 6 + ignoreDefaultParameters: false + MethodOverloading: + active: false + threshold: 6 + NestedBlockDepth: + active: true + threshold: 4 + StringLiteralDuplication: + active: false + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + thresholdInFiles: 15 + thresholdInClasses: 15 + thresholdInInterfaces: 15 + thresholdInObjects: 15 + thresholdInEnums: 11 + ignoreDeprecated: true + ignorePrivate: false + ignoreOverridden: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: false + methodNames: [toString,hashCode,equals,finalize] + InstanceOfCheckForException: + active: false + NotImplementedDeclaration: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: false + ReturnFromFinally: + active: false + SwallowedException: + active: false + ignoredExceptionTypes: [InterruptedException,NumberFormatException,ParseException,MalformedURLException] + ThrowingExceptionFromFinally: + active: false + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + exceptions: [IllegalArgumentException,IllegalStateException,IOException] + ThrowingNewInstanceOfSameException: + active: false + TooGenericExceptionCaught: + active: false + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + TooGenericExceptionThrown: + active: false + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +naming: + active: true + ClassNaming: + active: true + classPattern: '[A-Z$][a-zA-Z0-9$]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + functionPattern: '^([a-z$A-Z][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreOverridden: true + excludes: + - "**/*Test.kt" + - "**/*IT.kt" + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: false + ignoreOverridden: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + TopLevelPropertyNaming: + active: false + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: false + ForEachOnRange: + active: true + SpreadOperator: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: false + IteratorHasNextCallsNextMethod: + active: false + IteratorNotThrowingNoSuchElementException: + active: false + LateinitUsage: + active: false + ignoreAnnotated: [] + ignoreOnClassesPattern: "" + UnconditionalJumpStatementInLoop: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: false + UnsafeCast: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: false + +style: + active: true + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: [to] + EqualsNullCall: + active: false + EqualsOnSignatureLine: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: true + values: 'TODO:,FIXME:,STOPSHIP:' + ForbiddenImport: + active: false + imports: [] + ForbiddenVoid: + active: false + FunctionOnlyReturningConstant: + active: false + ignoreOverridableFunction: true + excludedFunctions: [describeContents] + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MagicNumber: + active: true + ignoreNumbers: ["-1","0","1","2"] + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + excludes: + - "**/*Test.kt" + - "**/*IT.kt" + MandatoryBracesIfStatements: + active: false + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: false + ModifierOrder: + active: true + NestedClassesVisibility: + active: false + NewLineAtEndOfFile: + active: false + NoTabs: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: false + max: 2 + excludedFunctions: [equals] + excludeLabeled: false + excludeReturnFromLambda: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: false + SpacingBetweenPackageAndImports: + active: false + ThrowsCount: + active: true + max: 2 + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableLength: 5 + UnnecessaryAbstractClass: + active: false + ignoreAnnotated: + - "dagger.Module" + UnnecessaryApply: + active: false + UnnecessaryInheritance: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: false + UnusedPrivateMember: + active: false + allowedNames: "(_|ignored|expected|serialVersionUID)" + UseDataClass: + active: false + ignoreAnnotated: [] + UtilityClassWithPublicConstructor: + active: false + VarCouldBeVal: + active: false + WildcardImport: + active: true + excludeImports: [java.util.*,kotlinx.android.synthetic.*] From 8b4bd1048c3705fdd48286738f34d72883439f91 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 08:40:58 +0300 Subject: [PATCH 81/96] add bsd-2-clause Signed-off-by: alperozturk96 --- LICENSES/BSD-2-Clause.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 LICENSES/BSD-2-Clause.txt diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 000000000..5f662b354 --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) + +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. From 5211bd05dc980b1ded06598fba83d44c276bec78 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 08:51:29 +0300 Subject: [PATCH 82/96] better inner class name Signed-off-by: alperozturk96 --- .../owncloud/notes/itemdetails/ItemDetailsActivity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index b43794b66..19ec2831b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -73,13 +73,13 @@ class ItemDetailsActivity : BrandedActivity() { private fun loadFragments() { val pages = listOf( - Page( + PageTab( PAGE.ACTIVITIES, getString(R.string.activities_title), AppCompatResources.getDrawable(this, R.drawable.nav_activity), ActivitiesFragment.newInstance(note, account) ), - Page( + PageTab( PAGE.SHARE, getString(R.string.share), AppCompatResources.getDrawable(this, R.drawable.ic_share_white_24dp), @@ -106,14 +106,14 @@ class ItemDetailsActivity : BrandedActivity() { util.material.themeTabLayout(binding.tabLayout) } - private data class Page ( + private data class PageTab( val page: PAGE, val title: String, val icon: Drawable?, val fragment: BrandedFragment ) - private inner class PagerAdapter(val fragments: List) : + private inner class PagerAdapter(val fragments: List) : FragmentStateAdapter(this@ItemDetailsActivity) { override fun getItemCount() = fragments.size From 53a295cfbed21daa27c8500d75c275a8fef70b07 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 08:55:30 +0300 Subject: [PATCH 83/96] align bottom sheet icons Signed-off-by: alperozturk96 --- app/src/main/res/layout/item_action_bottom_sheet.xml | 3 ++- app/src/main/res/values/dimens.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/item_action_bottom_sheet.xml b/app/src/main/res/layout/item_action_bottom_sheet.xml index 6daf4abbb..9a521e51f 100644 --- a/app/src/main/res/layout/item_action_bottom_sheet.xml +++ b/app/src/main/res/layout/item_action_bottom_sheet.xml @@ -28,6 +28,7 @@ android:layout_marginBottom="@dimen/spacer_2x" android:gravity="center_vertical" android:orientation="horizontal" + android:paddingStart="@dimen/standard_half_padding" android:visibility="visible"> 196dip 48dp 40dp + 32dp 16sp 56dp 24dp From 6b9e69cb77ef682df241dc096e6516367419a9a2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 09:01:03 +0300 Subject: [PATCH 84/96] fix codacy Signed-off-by: alperozturk96 --- app/build.gradle.kts | 1 + .../notes/activities/ActivitiesFragment.kt | 3 +- .../notes/itemdetails/ItemDetailsActivity.kt | 4 +- .../notes/persistence/entity/ShareEntity.kt | 1 + .../notes/persistence/sync/ShareAPI.kt | 5 ++- .../notes/share/NoteShareDetailActivity.kt | 1 + .../owncloud/notes/share/NoteShareFragment.kt | 37 ++++++++----------- .../notes/share/adapter/SuggestionAdapter.kt | 1 + .../share/dialog/ProfileBottomSheetDialog.kt | 1 + ...hareLinkDialog.kt => ShareLinkToDialog.kt} | 0 .../notes/share/model/UpdateShareRequest.kt | 5 ++- .../notes/share/repository/ShareRepository.kt | 4 +- .../notes/shared/util/ExtendedFabUtil.kt | 4 +- .../notes/shared/util/NoteUtilTest.kt | 3 +- 14 files changed, 41 insertions(+), 29 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/share/dialog/{ShareLinkDialog.kt => ShareLinkToDialog.kt} (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1dea2379a..6298b8fba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ * 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") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index 56cc3a3d4..a7aef9059 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -109,7 +109,8 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie withContext(Dispatchers.Main) { when (result) { RestoreVersionsResult.Success -> syncNote() - RestoreVersionsResult.FailedUnsupported -> showMessageAndHideProgress(R.string.version_restore_failed_unsupported) + RestoreVersionsResult.FailedUnsupported -> + showMessageAndHideProgress(R.string.version_restore_failed_unsupported) else -> showMessageAndHideProgress(R.string.version_restore_failed) } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 19ec2831b..c9e388e1d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -47,13 +47,15 @@ class ItemDetailsActivity : BrandedActivity() { loadFragments() } + @Suppress("ThrowsCount") + private fun parseArguments() { note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") page = intent.extras.getSerializableArgument(ARG_PAGE, PAGE::class.java) - ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") + ?: throw RuntimeException("Argument $ARG_PAGE is mandatory") } private fun loadHeaderData() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt index 759d1f7fa..e0c270f4b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt @@ -10,6 +10,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +@Suppress("ConstructorParameterNaming") @Entity(tableName = "share_table") data class ShareEntity( @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt index c6a15e80e..98ffb02ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt @@ -47,7 +47,10 @@ interface ShareAPI { fun addShare(@Body request: CreateShareRequest): Call> @PUT("shares/{shareId}?format=json") - fun updateShare(@Path("shareId") shareId: Long, @Body request: UpdateShareRequest): Call> + fun updateShare( + @Path("shareId") shareId: Long, + @Body request: UpdateShareRequest + ): Call> @PUT("shares/{shareId}?format=json") fun updateSharePermission( diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt index 3e5a6ceb2..0f25393b6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt @@ -626,6 +626,7 @@ class NoteShareDetailActivity : } } + @Suppress("ForbiddenComment") private suspend fun createShare(password: String) { if (note == null || shareeName == null) { Log_OC.d(TAG, "validateShareProcessSecond cancelled") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt index 179eeab85..706f0193a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.share import android.Manifest @@ -83,13 +89,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import kotlin.text.toInt - -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ +@Suppress("LargeClass") class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShareItemAction, QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { private var executorService: ScheduledExecutorService? = null @@ -384,7 +384,9 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val result: ApiResult?> = repository!!.addShare(note!!, ShareType.PUBLIC_LINK, "") activity?.runOnUiThread { - if (result is ApiResult.Success?> && binding!!.sharesList.adapter is ShareeListAdapter) { + if (result is ApiResult.Success?> && + binding!!.sharesList.adapter is ShareeListAdapter + ) { DisplayUtils.showSnackMessage(view, result.message) note!!.setIsShared(true) repository!!.updateNote(note!!) @@ -403,9 +405,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } - // TODO: - override fun createSecureFileDrop() { - } + override fun createSecureFileDrop() = Unit private fun showShareLinkDialog(link: String?) { val intentToShareLink = Intent(Intent.ACTION_SEND) @@ -417,8 +417,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar getString(R.string.note_share_activity_subject_shared_with_you, note!!.title) ) - val packagesToExclude = arrayOf(context?.packageName) - val chooserDialog: DialogFragment = newInstance(intentToShareLink, *packagesToExclude) + val chooserDialog: DialogFragment = newInstance(intentToShareLink, context?.packageName) chooserDialog.show(parentFragmentManager, FTAG_CHOOSER_DIALOG) } @@ -481,9 +480,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) } - // TODO: - override fun showProfileBottomSheet(account: Account?, shareWith: String?) { - } + override fun showProfileBottomSheet(account: Account?, shareWith: String?) = Unit private var isInitializingShares = false @@ -562,7 +559,6 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val context = context ?: return val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) - // FIXME: if (intent.resolveActivity(context.packageManager) != null) { onContactSelectionResultLauncher.launch(intent) } else { @@ -663,9 +659,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar }) } - // TODO: - override fun sendLink(share: OCShare?) { - } + override fun sendLink(share: OCShare?) = Unit override fun addAnotherLink(share: OCShare?) { createPublicShareLink() @@ -696,7 +690,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar val majorVersionAsString = capabilities!!.nextcloudMajorVersion if (majorVersionAsString != null) { val majorVersion = majorVersionAsString.toInt() - return majorVersion >= 18 + return majorVersion >= EXPIRATION_DATE_MIN_MAJOR_VERSION } return false @@ -893,6 +887,7 @@ class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShar const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" private const val SEARCH_DELAY_MS: Long = 500 + private const val EXPIRATION_DATE_MIN_MAJOR_VERSION = 18 fun newInstance(note: Note, account: Account) : NoteShareFragment { val f = NoteShareFragment() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt index 08d10bd5b..77483feaf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt @@ -43,6 +43,7 @@ class SuggestionAdapter(context: Context, cursor: Cursor?, private val account: return binding.root } + @Suppress("NestedBlockDepth") override fun bindView(view: View, context: Context, cursor: Cursor) { val suggestion = cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt index 615e13933..ffb118973 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt @@ -28,6 +28,7 @@ import it.niedermann.owncloud.notes.shared.util.DisplayUtils /** * Show actions of an user */ +@Suppress("ForbiddenComment") class ProfileBottomSheetDialog( private val fileActivity: FragmentActivity, private val account: Account, diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkDialog.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkToDialog.kt similarity index 100% rename from app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkDialog.kt rename to app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkToDialog.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt index ec9f8c10f..5d6d4d27a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt @@ -43,12 +43,15 @@ data class UpdateShareRequest( val attributes: String? = null ) { companion object { + const val SHARE_ATTRIBUTES_V2_MIN_VERSION = 30 + fun createAttributes(capabilities: Capabilities, allowDownloadAndSync: Boolean, type: ShareType?): String { if (type == null || (type != ShareType.INTERNAL && type != ShareType.USER)) { return "[]" } - val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30 + val shouldUseShareAttributesV2 = + (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= SHARE_ATTRIBUTES_V2_MIN_VERSION val shareAttributes = arrayOf( if (shouldUseShareAttributesV2) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt index 442fd8b5e..9e7aac04f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt @@ -68,6 +68,7 @@ class ShareRepository(private val applicationContext: Context, private val accou fun getCapabilities(): Capabilities = notesRepository.capabilities // region API calls + @Suppress("NestedBlockDepth") fun fetchSharesForNotesAndSaveShareEntities() { val notesPathResponseResult = getNotesPathResponseResult() ?: return val notesPath = notesPathResponseResult.notesPath @@ -135,7 +136,8 @@ class ShareRepository(private val applicationContext: Context, private val accou val entity = getShareByPathAndDisplayName(share) val attributes = entity?.attributes ?: return false val capabilities = notesRepository.capabilities - val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30 + val shouldUseShareAttributesV2 = + (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= UpdateShareRequest.SHARE_ATTRIBUTES_V2_MIN_VERSION return if (shouldUseShareAttributesV2) { ShareAttributesV2.getAttributes(attributes).first().value } else { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt index d201e2d0b..c8ade93f5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ExtendedFabUtil.kt @@ -32,13 +32,13 @@ object ExtendedFabUtil { ) animation.setAnimationListener( object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) {} + override fun onAnimationStart(animation: Animation) = Unit override fun onAnimationEnd(animation: Animation) { extendedFab.visibility = View.GONE } - override fun onAnimationRepeat(animation: Animation) {} + override fun onAnimationRepeat(animation: Animation) = Unit }, ) extendedFab.startAnimation(animation) diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt index ed2495f92..928a1297d 100644 --- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt +++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt @@ -82,7 +82,8 @@ class NoteUtilTest : TestCase() { } private fun testTitleMatchingExcerpts() { - // content actually starts with title → return max. 200 characters starting with the first character after the title + // content actually starts with title → + // return max. 200 characters starting with the first character after the title assertEquals("", NoteUtil.generateNoteExcerpt("Title", "Title")) assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\nFoo", "Title")) assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("Title\nTitle\nBar", "Title")) From 343cdea7a1618836c60caab3a09175428485d32b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 09:07:59 +0300 Subject: [PATCH 85/96] fix background color Signed-off-by: alperozturk96 --- .../owncloud/notes/itemdetails/ItemDetailsActivity.kt | 2 +- .../it/niedermann/owncloud/notes/main/MainActivity.java | 2 +- .../notes/main/MultiSelectedActionModeCallback.java | 8 ++++++++ app/src/main/res/layout/fragment_activities.xml | 1 - 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index c9e388e1d..20da17d89 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -48,7 +48,6 @@ class ItemDetailsActivity : BrandedActivity() { } @Suppress("ThrowsCount") - private fun parseArguments() { note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") @@ -103,6 +102,7 @@ class ItemDetailsActivity : BrandedActivity() { override fun applyBrand(color: Int) { val util = BrandingUtil.of(color, this) util.material.themeToolbar(binding.toolbar) + util.platform.colorViewBackground(window.decorView) util.platform.colorViewBackground(binding.getRoot()) util.platform.colorViewBackground(binding.tabLayout) util.material.themeTabLayout(binding.tabLayout) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 0c65ce160..11c2b4673 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -718,7 +718,7 @@ public void applyBrand(int color) { util.platform.colorNavigationView(binding.navigationView); util.material.themeFAB(activityBinding.fabCreate); util.notes.themeSearchCardView(binding.activityNotesListView.searchBar.searchBarCardWrapper); - util.platform.colorViewBackground(getWindow().getDecorView(), ColorRole.SURFACE_CONTAINER); + if (mActionMode == null) util.platform.colorViewBackground(getWindow().getDecorView(), ColorRole.SURFACE_CONTAINER); util.platform.colorViewBackground(binding.getRoot(), ColorRole.SURFACE_CONTAINER); util.platform.colorViewBackground(binding.activityNotesListView.activityNotesListView, ColorRole.SURFACE_CONTAINER); util.platform.colorTextView(binding.activityNotesListView.searchBar.searchText, ColorRole.ON_SURFACE_VARIANT); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index 470d3e7e1..6af623a80 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -8,6 +8,8 @@ import android.content.Context; import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; @@ -17,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ActionMode.Callback; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; @@ -57,6 +60,7 @@ public class MultiSelectedActionModeCallback implements Callback { private final FragmentManager fragmentManager; @NonNull private final MainActivity mainActivity; + private Drawable systemBarsBackground; public MultiSelectedActionModeCallback( @NonNull MainActivity mainActivity, @@ -85,6 +89,9 @@ public MultiSelectedActionModeCallback( @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { + final var decorView = mainActivity.getWindow().getDecorView(); + systemBarsBackground = decorView.getBackground(); + decorView.setBackground(new ColorDrawable(ContextCompat.getColor(context, R.color.action_mode_background))); // inflate contextual menu mode.getMenuInflater().inflate(R.menu.menu_list_context_multiple, menu); menu.findItem(R.id.menu_move).setVisible(canMoveNoteToAnotherAccounts); @@ -200,6 +207,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { @Override public void onDestroyActionMode(ActionMode mode) { + mainActivity.getWindow().getDecorView().setBackground(systemBarsBackground); if (mode != null) { mode.finish(); } diff --git a/app/src/main/res/layout/fragment_activities.xml b/app/src/main/res/layout/fragment_activities.xml index 14d95e6ac..b7026aa56 100644 --- a/app/src/main/res/layout/fragment_activities.xml +++ b/app/src/main/res/layout/fragment_activities.xml @@ -20,7 +20,6 @@ android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/bg_default" android:clipToPadding="false" android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" From 82453cbe0617cecce26ec731b86bd2b4f4983b7a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 09:17:49 +0300 Subject: [PATCH 86/96] optimize imports Signed-off-by: alperozturk96 --- .../notes/activities/repository/ActivitiesRepository.kt | 1 - .../notes/activities/repository/RestoreVersionsResult.kt | 2 -- .../owncloud/notes/activities/repository/VersionsRepository.kt | 1 - 3 files changed, 4 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index 3d631112e..d46c64b56 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -13,7 +13,6 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount import it.niedermann.owncloud.notes.persistence.ApiProvider import okio.IOException import java.net.HttpURLConnection -import kotlin.getValue class ActivitiesRepository( private val context: Context, diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt index b9844efa0..a2d520655 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt @@ -7,8 +7,6 @@ */ package it.niedermann.owncloud.notes.activities.repository -import com.owncloud.android.lib.resources.files.model.FileVersion - enum class RestoreVersionsResult{ Success, Failed, FailedUnsupported } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index 1d3203bbc..ce6a622b0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -11,7 +11,6 @@ import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.persistence.ApiProvider -import it.niedermann.owncloud.notes.persistence.sync.VersionsAPI class VersionsRepository ( private val context: Context, From 89186e9eba54d8e13dc0857bf054d346f8e10f0a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 09:20:49 +0300 Subject: [PATCH 87/96] move license header to top of the file Signed-off-by: alperozturk96 --- .../notes/activities/ActivitiesFragment.kt | 14 +++++++------- .../owncloud/notes/itemactions/NoteAction.kt | 13 ++++++------- .../notes/itemdetails/ItemDetailsActivity.kt | 13 ++++++------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index a7aef9059..e11e7ea89 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * 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 @@ -30,13 +37,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2026 Daniele Verducci - * 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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt index 84d27e89a..bbefa9e53 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemactions/NoteAction.kt @@ -1,10 +1,3 @@ -package it.niedermann.owncloud.notes.itemactions - -import androidx.annotation.DrawableRes -import androidx.annotation.IdRes -import androidx.annotation.StringRes -import it.niedermann.owncloud.notes.R - /* * Nextcloud Notes - Android Client * @@ -12,6 +5,12 @@ import it.niedermann.owncloud.notes.R * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ +package it.niedermann.owncloud.notes.itemactions + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import it.niedermann.owncloud.notes.R enum class NoteAction( @param:IdRes val id: Int, diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt index 20da17d89..a84bc92c7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.itemdetails import android.content.Context @@ -19,13 +25,6 @@ import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.share.NoteShareFragment import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument - -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ class ItemDetailsActivity : BrandedActivity() { lateinit var binding : ActivityItemDetailsBinding From f48e84e043478b5dea1149be5b247cff6836f3db Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:28:38 +0100 Subject: [PATCH 88/96] Converted ActivitiesData to data class Signed-off-by: daniele-verducci --- .../notes/activities/adapter/ActivitiesData.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index b00b5646a..fa67e4ace 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -11,12 +11,12 @@ 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() - val versions = arrayListOf() - var activitiesLastGiven: Long? = null +data class ActivitiesData( + val activities: ArrayList = arrayListOf(), + val versions: ArrayList = arrayListOf(), + var activitiesLastGiven: Long? = null, var activitiesMore: Boolean = true - +) { fun getAggregatedList(context: Context) : List { val items = arrayListOf() items.addAll( @@ -30,4 +30,4 @@ class ActivitiesData { } fun isEmpty() : Boolean = activities.isEmpty() && versions.isEmpty() -} \ No newline at end of file +} From 41b6916827c4b38348457f8291088d14def3798d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:29:28 +0100 Subject: [PATCH 89/96] Moved activity model into adapter pkg Signed-off-by: daniele-verducci --- .../niedermann/owncloud/notes/activities/ActivitiesFragment.kt | 2 +- .../owncloud/notes/activities/adapter/ActivitiesData.kt | 2 +- .../owncloud/notes/activities/adapter/ActivityListAdapter.kt | 2 +- .../owncloud/notes/activities/adapter/ActivityListItem.kt | 2 +- .../notes/activities/adapter/OnActivityClickListener.kt | 2 +- .../owncloud/notes/activities/{ => adapter}/model/Activity.kt | 2 +- .../owncloud/notes/activities/repository/ActivitiesResult.kt | 2 +- .../niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/activities/{ => adapter}/model/Activity.kt (92%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index e11e7ea89..fd78a7106 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -21,7 +21,7 @@ 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.adapter.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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index fa67e4ace..ae24c932e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -9,7 +9,7 @@ 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 +import it.niedermann.owncloud.notes.activities.adapter.model.Activity data class ActivitiesData( val activities: ArrayList = arrayListOf(), diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 58f9de8d6..88f4bc7dc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -17,7 +17,7 @@ import com.bumptech.glide.Glide import com.nextcloud.android.common.core.utils.DateFormatter import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.model.Activity import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandedViewHolder import it.niedermann.owncloud.notes.branding.BrandingUtil diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt index ea1ebc49f..d0c422476 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt @@ -10,7 +10,7 @@ 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.R -import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.model.Activity import it.niedermann.owncloud.notes.util.HumanReadableFormatter sealed class ActivityListItem : Comparable { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt index 0893426ef..c84d2d6c8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/OnActivityClickListener.kt @@ -8,7 +8,7 @@ package it.niedermann.owncloud.notes.activities.adapter import com.owncloud.android.lib.resources.files.model.FileVersion -import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.model.Activity interface OnActivityClickListener { fun onActivityItemClick(activity: Activity) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/Activity.kt similarity index 92% rename from app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt rename to app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/Activity.kt index 7874f4cba..3cd1a16f4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/model/Activity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/Activity.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -package it.niedermann.owncloud.notes.activities.model +package it.niedermann.owncloud.notes.activities.adapter.model import com.google.gson.annotations.SerializedName import java.util.Date diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt index 34a6e8632..08774b9b1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt @@ -8,7 +8,7 @@ package it.niedermann.owncloud.notes.activities.repository import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.model.Activity sealed class ActivitiesResult(open val descriptionId: Int? = null) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt index 15611bf20..4c47d8202 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ActivitiesAPI.kt @@ -7,7 +7,7 @@ */ package it.niedermann.owncloud.notes.persistence.sync -import it.niedermann.owncloud.notes.activities.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.model.Activity import it.niedermann.owncloud.notes.shared.model.OcsResponse import retrofit2.Call import retrofit2.http.GET From aa84855e4fb492ded7e08a08e8d783aac7e67520 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:32:02 +0100 Subject: [PATCH 90/96] Moved ActivityViewHolder to its own file Signed-off-by: daniele-verducci --- .../activities/adapter/ActivityListAdapter.kt | 77 +--------------- .../adapter/viewholder/ActivityViewHolder.kt | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 88f4bc7dc..236a7b1a7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -18,6 +18,7 @@ import com.nextcloud.android.common.core.utils.DateFormatter import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.adapter.model.Activity +import it.niedermann.owncloud.notes.activities.adapter.viewholder.ActivityViewHolder import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandedViewHolder import it.niedermann.owncloud.notes.branding.BrandingUtil @@ -69,80 +70,4 @@ class ActivityListAdapter ( brandColor = color } - private class ActivityViewHolder( - val binding: ActivityListItemBinding, - val dateFormatter: DateFormatter, - val calendar: Calendar - ) : BrandedViewHolder(binding.root) { - - fun bind(item: ActivityListItem, listener: OnActivityClickListener) { - bindDateTime(item) - bindSubject(item) - bindMessage(item) - bindIcon(item) - bindActionIcon(item) - binding.root.setOnClickListener { - when (item) { - is ActivityListItem.ActivityItem -> listener.onActivityItemClick(item.activity) - is ActivityListItem.FileVersionItem -> listener.onVersionItemClick(item.fileVersion) - } - } - } - - private fun bindDateTime(activity: ActivityListItem) { - binding.datetime.apply { - visibility = View.VISIBLE - text = dateFormatter.getConditionallyRelativeFormattedTimeSpan( - calendar.apply { - timeInMillis = activity.getTimestamp() - } - ) - } - } - - private fun bindSubject(activity: ActivityListItem) { - binding.subject.text = activity.getTitle() - } - - private fun bindMessage(activity: ActivityListItem) { - binding.message.apply { - when { - activity.getMessage().isNullOrEmpty() -> visibility = View.GONE - else -> { - visibility = View.VISIBLE - text = activity.getMessage() - } - } - } - } - - private fun bindIcon(activity: ActivityListItem) { - val iconUrl = activity.getIconUrl() - if (iconUrl.isNullOrEmpty()) { - binding.icon.setImageResource(R.drawable.ic_activity) - return - } - - Glide - .with(binding.icon.context) - .load(iconUrl.toUri()) - .placeholder( R.drawable.ic_activity) - .error(R.drawable.ic_activity) - .into(binding.icon) - } - - private fun bindActionIcon(activity: ActivityListItem) { - binding.actionIcon.visibility = - if (activity.canBeRestored()) View.VISIBLE - else View.GONE - } - - override fun applyBrand(color: Int) { - val util = BrandingUtil.of(color, binding.root.context) - util.platform.colorViewBackground(binding.root) - util.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) - } - - } - } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt new file mode 100644 index 000000000..f29172798 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt @@ -0,0 +1,91 @@ +package it.niedermann.owncloud.notes.activities.adapter.viewholder + +import android.view.View +import androidx.core.net.toUri +import com.bumptech.glide.Glide +import com.nextcloud.android.common.core.utils.DateFormatter +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.activities.adapter.ActivityListItem +import it.niedermann.owncloud.notes.activities.adapter.OnActivityClickListener +import it.niedermann.owncloud.notes.branding.BrandedViewHolder +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding +import java.util.Calendar + + +class ActivityViewHolder( + val binding: ActivityListItemBinding, + val dateFormatter: DateFormatter, + val calendar: Calendar +) : BrandedViewHolder(binding.root) { + + fun bind(item: ActivityListItem, listener: OnActivityClickListener) { + bindDateTime(item) + bindSubject(item) + bindMessage(item) + bindIcon(item) + bindActionIcon(item) + binding.root.setOnClickListener { + when (item) { + is ActivityListItem.ActivityItem -> listener.onActivityItemClick(item.activity) + is ActivityListItem.FileVersionItem -> listener.onVersionItemClick(item.fileVersion) + } + } + } + + private fun bindDateTime(activity: ActivityListItem) { + binding.datetime.apply { + visibility = View.VISIBLE + text = dateFormatter.getConditionallyRelativeFormattedTimeSpan( + calendar.apply { + timeInMillis = activity.getTimestamp() + } + ) + } + } + + private fun bindSubject(activity: ActivityListItem) { + binding.subject.text = activity.getTitle() + } + + private fun bindMessage(activity: ActivityListItem) { + binding.message.apply { + when { + activity.getMessage().isNullOrEmpty() -> visibility = View.GONE + else -> { + visibility = View.VISIBLE + text = activity.getMessage() + } + } + } + } + + private fun bindIcon(activity: ActivityListItem) { + val iconUrl = activity.getIconUrl() + if (iconUrl.isNullOrEmpty()) { + binding.icon.setImageResource(R.drawable.ic_activity) + return + } + + Glide + .with(binding.icon.context) + .load(iconUrl.toUri()) + .placeholder( R.drawable.ic_activity) + .error(R.drawable.ic_activity) + .into(binding.icon) + } + + private fun bindActionIcon(activity: ActivityListItem) { + binding.actionIcon.visibility = + if (activity.canBeRestored()) View.VISIBLE + else View.GONE + } + + override fun applyBrand(color: Int) { + val util = BrandingUtil.of(color, binding.root.context) + util.platform.colorViewBackground(binding.root) + util.platform.colorImageView(binding.icon, ColorRole.ON_SURFACE_VARIANT) + } + +} \ No newline at end of file From 0547caa221beb3996910e2a8fdd48fc276af82a4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:34:40 +0100 Subject: [PATCH 91/96] Moved ActivityListItem into model Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/adapter/ActivitiesData.kt | 4 +++- .../notes/activities/adapter/ActivityListAdapter.kt | 8 +------- .../activities/adapter/{ => model}/ActivityListItem.kt | 3 +-- .../activities/adapter/viewholder/ActivityViewHolder.kt | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/{ => model}/ActivityListItem.kt (92%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt index ae24c932e..12c2712e6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivitiesData.kt @@ -10,6 +10,8 @@ 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 = arrayListOf(), @@ -23,7 +25,7 @@ data class ActivitiesData( activities.map { ActivityListItem.ActivityItem(it) } ) items.addAll( - versions.map { ActivityListItem.FileVersionItem(context, it) } + versions.map { FileVersionItem(context, it) } ) items.sortDescending() return items diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt index 236a7b1a7..f776ef29a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListAdapter.kt @@ -9,19 +9,13 @@ package it.niedermann.owncloud.notes.activities.adapter import android.content.Context 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 com.nextcloud.android.common.core.utils.DateFormatter -import com.nextcloud.android.common.ui.theme.utils.ColorRole -import it.niedermann.owncloud.notes.R 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.viewholder.ActivityViewHolder import it.niedermann.owncloud.notes.branding.Branded -import it.niedermann.owncloud.notes.branding.BrandedViewHolder -import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityListItemBinding import java.util.Calendar diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/ActivityListItem.kt similarity index 92% rename from app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt rename to app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/ActivityListItem.kt index d0c422476..791830338 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/ActivityListItem.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/model/ActivityListItem.kt @@ -5,12 +5,11 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -package it.niedermann.owncloud.notes.activities.adapter +package it.niedermann.owncloud.notes.activities.adapter.model import android.content.Context import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.activities.adapter.model.Activity import it.niedermann.owncloud.notes.util.HumanReadableFormatter sealed class ActivityListItem : Comparable { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt index f29172798..52f6f2a6c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt @@ -6,7 +6,7 @@ import com.bumptech.glide.Glide import com.nextcloud.android.common.core.utils.DateFormatter import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.activities.adapter.ActivityListItem +import it.niedermann.owncloud.notes.activities.adapter.model.ActivityListItem import it.niedermann.owncloud.notes.activities.adapter.OnActivityClickListener import it.niedermann.owncloud.notes.branding.BrandedViewHolder import it.niedermann.owncloud.notes.branding.BrandingUtil From ce3510baac830d72a5936edb3e7988e23c63553b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:43:30 +0100 Subject: [PATCH 92/96] Moved VersionsResult into model Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 2 +- .../owncloud/notes/activities/repository/VersionsRepository.kt | 1 + .../notes/activities/repository/{ => model}/VersionsResult.kt | 2 +- .../niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/activities/repository/{ => model}/VersionsResult.kt (87%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index c2b06f0bd..433bfcab0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -16,7 +16,7 @@ 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 it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index ce6a622b0..96e916709 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -10,6 +10,7 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.resources.files.model.FileVersion +import it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import it.niedermann.owncloud.notes.persistence.ApiProvider class VersionsRepository ( diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/VersionsResult.kt similarity index 87% rename from app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt rename to app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/VersionsResult.kt index 96f4b167e..f525224ab 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/VersionsResult.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -package it.niedermann.owncloud.notes.activities.repository +package it.niedermann.owncloud.notes.activities.repository.model import com.owncloud.android.lib.resources.files.model.FileVersion diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index b69de8312..831ddd55d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -17,7 +17,7 @@ import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.lib.resources.files.model.FileVersion import it.niedermann.owncloud.notes.activities.repository.RestoreVersionsResult -import it.niedermann.owncloud.notes.activities.repository.VersionsResult +import it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import org.apache.jackrabbit.webdav.DavConstants import org.apache.jackrabbit.webdav.MultiStatus import org.apache.jackrabbit.webdav.client.methods.PropFindMethod From e0eebdf3e6288ea3fa5b0e5a14bbfcb5e6a51f8c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:46:11 +0100 Subject: [PATCH 93/96] Moved ActivitiesResult into model Signed-off-by: daniele-verducci --- .../notes/activities/repository/{ => model}/ActivitiesResult.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/activities/repository/{ => model}/ActivitiesResult.kt (100%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt similarity index 100% rename from app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesResult.kt rename to app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt From 9698d70aec53b97b0338abf746041358b2c6d0d4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:47:17 +0100 Subject: [PATCH 94/96] Moved RestoreVersionsResult into model Signed-off-by: daniele-verducci --- .../owncloud/notes/activities/ActivitiesDataProvider.kt | 2 +- .../niedermann/owncloud/notes/activities/ActivitiesFragment.kt | 2 +- .../notes/activities/repository/ActivitiesRepository.kt | 1 + .../owncloud/notes/activities/repository/VersionsRepository.kt | 1 + .../notes/activities/repository/model/ActivitiesResult.kt | 2 +- .../activities/repository/{ => model}/RestoreVersionsResult.kt | 2 +- .../niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt | 2 +- 7 files changed, 7 insertions(+), 5 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/activities/repository/{ => model}/RestoreVersionsResult.kt (83%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt index 433bfcab0..26568ec9f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesDataProvider.kt @@ -14,7 +14,7 @@ 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.model.ActivitiesResult import it.niedermann.owncloud.notes.activities.repository.VersionsRepository import it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index fd78a7106..e7b524295 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -23,7 +23,7 @@ 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.RestoreVersionsResult +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 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt index d46c64b56..032ee1215 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/ActivitiesRepository.kt @@ -10,6 +10,7 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context import android.util.Log import com.nextcloud.android.sso.model.SingleSignOnAccount +import it.niedermann.owncloud.notes.activities.repository.model.ActivitiesResult import it.niedermann.owncloud.notes.persistence.ApiProvider import okio.IOException import java.net.HttpURLConnection diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt index 96e916709..9878894e1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/VersionsRepository.kt @@ -10,6 +10,7 @@ package it.niedermann.owncloud.notes.activities.repository import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.resources.files.model.FileVersion +import it.niedermann.owncloud.notes.activities.repository.model.RestoreVersionsResult import it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import it.niedermann.owncloud.notes.persistence.ApiProvider diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt index 08774b9b1..9dcd61df6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/ActivitiesResult.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -package it.niedermann.owncloud.notes.activities.repository +package it.niedermann.owncloud.notes.activities.repository.model import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.activities.adapter.model.Activity diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/RestoreVersionsResult.kt similarity index 83% rename from app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt rename to app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/RestoreVersionsResult.kt index a2d520655..bfc24e564 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/RestoreVersionsResult.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/repository/model/RestoreVersionsResult.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -package it.niedermann.owncloud.notes.activities.repository +package it.niedermann.owncloud.notes.activities.repository.model enum class RestoreVersionsResult{ Success, Failed, FailedUnsupported diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt index 831ddd55d..bed0e5984 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/VersionsAPI.kt @@ -16,7 +16,7 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.lib.resources.files.model.FileVersion -import it.niedermann.owncloud.notes.activities.repository.RestoreVersionsResult +import it.niedermann.owncloud.notes.activities.repository.model.RestoreVersionsResult import it.niedermann.owncloud.notes.activities.repository.model.VersionsResult import org.apache.jackrabbit.webdav.DavConstants import org.apache.jackrabbit.webdav.MultiStatus From 64c7b2613256e1f23167c26cca6db4c1899391c6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 09:49:55 +0100 Subject: [PATCH 95/96] More kotlin-idiomatic function Signed-off-by: daniele-verducci --- .../notes/activities/ActivitiesFragment.kt | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt index e7b524295..a9bdd561a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -94,9 +94,7 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie util.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) } - override fun onActivityItemClick(activity: Activity) { - // No action - } + override fun onActivityItemClick(activity: Activity) = Unit override fun onVersionItemClick(version: FileVersion) { showProgress(true) @@ -170,15 +168,11 @@ class ActivitiesFragment : BrandedFragment(), OnActivityClickListener, Activitie 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 + fun newInstance(note: Note, account: Account) : ActivitiesFragment = ActivitiesFragment().apply { + arguments = Bundle().apply { + putSerializable(ARG_NOTE, note) + putSerializable(ARG_ACCOUNT, account) + } } } } \ No newline at end of file From 25a1d14d1f740b7a9ece4be817fcf4656c5bf8a7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 3 Nov 2026 10:36:57 +0100 Subject: [PATCH 96/96] Missing license Signed-off-by: daniele-verducci --- .../activities/adapter/viewholder/ActivityViewHolder.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt index 52f6f2a6c..95c1a4fe8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/adapter/viewholder/ActivityViewHolder.kt @@ -1,3 +1,10 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package it.niedermann.owncloud.notes.activities.adapter.viewholder import android.view.View