From b00b6b2f34d94844b933759809a87d714ddf2217 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:11:41 +0200 Subject: [PATCH 01/39] 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 4720d0c099ac49eb05e6d9966aad40343478e08d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:12:16 +0200 Subject: [PATCH 02/39] 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 8c8e50631..1df0837c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -539,4 +539,10 @@ Account not found! Activities + Note.md + Favorite + 389 KB +  • + 12:23:45 + From 9dd2668b19c25b305d1f912207df4e3eef80cd6e Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:22:17 +0200 Subject: [PATCH 03/39] 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 1df0837c9..df18976e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -539,6 +539,8 @@ Account not found! Activities + Activities + Note.md Favorite 389 KB From 2a1789fcd9fb2348f2aae0c4be1d9343d8ccb6e3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:34:08 +0200 Subject: [PATCH 04/39] 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 409e1303b43e39622cae73697323668cfbef856b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:36:25 +0200 Subject: [PATCH 05/39] 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 7c9022b8c6a745d50a9415c73869f51cb8a668cd Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 6 Oct 2026 18:44:50 +0200 Subject: [PATCH 06/39] 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 d0055f25d..be77ff090 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 ecedef2050d1e700838a9e96bab5c1bda7881e17 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 17:16:51 +0200 Subject: [PATCH 07/39] 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 7ad562b00f4161bb0bbc766f2f0ba5c671016050 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 17:30:32 +0200 Subject: [PATCH 08/39] 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 78a3b9d5f1c615a15e3102c5b0b8e5284cc4c284 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:22:27 +0200 Subject: [PATCH 09/39] 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/39] 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 b7a5d572fe819236736d055c7dc2bf4ef5f55ed4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:34:31 +0200 Subject: [PATCH 11/39] 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 83b2a52fb7d7691b87345cb7f1a1288460f71f06 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 12 Oct 2026 18:57:15 +0200 Subject: [PATCH 12/39] 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 ed7b84ca6..6967ab13f 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 @@ -114,7 +114,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 4a7bad12f8769ace0504c3d915668cfe2e61d660 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:14:57 +0200 Subject: [PATCH 13/39] 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 6967ab13f..f5c07fe61 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 @@ -100,6 +100,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; @@ -914,10 +915,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 f1f9590d91e89f4ace885985db6d1397649c78c0 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:21:57 +0200 Subject: [PATCH 14/39] 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 fb0cc62f064cd95c18f9735f25f7b62d9549cc68 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 10:52:47 +0200 Subject: [PATCH 15/39] 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 c232c742022d4794385ef44b7b404bbb14561b98 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 14:21:49 +0200 Subject: [PATCH 16/39] 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 5aeb155f0572ee414d4402a583d95a6fc4b74b73 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 14:40:45 +0200 Subject: [PATCH 17/39] 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/39] 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 2fdb54eeca0f1d7ae55e569e8d581a522ca6ee66 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 13 Oct 2026 17:39:41 +0200 Subject: [PATCH 19/39] 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 2be15a799d6782f94fbe4e261c038bdd8cb8da88 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 12:21:29 +0200 Subject: [PATCH 20/39] 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 96c9bfb375cd715dd9e052dd6874105db77c9cd6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 12:22:03 +0200 Subject: [PATCH 21/39] 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 9f0a08667c6581c1f44cbfe9ee7c2814554d3b99 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 13:09:28 +0200 Subject: [PATCH 22/39] 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 e5411bfed..73a6a11a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -539,7 +539,10 @@ Account not found! Details + Activities + Unable to fetch activities + No activity present for this note Note.md Favorite From 9968ae82db7808bef85b98a184b8544f789839ef Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 14:04:01 +0200 Subject: [PATCH 23/39] 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 c041832b22d44c667750a57690731a19ee0bebcc Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 16:45:11 +0200 Subject: [PATCH 24/39] 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 73a6a11a4..2281191aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -543,10 +543,13 @@ Activities Unable to fetch activities No activity present for this note + Activity Note.md Favorite  • 12:23:45 + This is a placeholder + From 4141dcb0d040df494ab3e33e627153778a11c74d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 16:58:52 +0200 Subject: [PATCH 25/39] 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 eb79668237f9f9ca39147aa4838e2e38f9437fae Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 17:21:00 +0200 Subject: [PATCH 26/39] 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 2281191aa..6633d809b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -544,6 +544,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 d0a1a10e8b1c8929fe3ffb69c563d126b4e39ef8 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 17:34:29 +0200 Subject: [PATCH 27/39] 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 9f739d06497676dbf80c3b84916820f8fcdac7c3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 18:14:58 +0200 Subject: [PATCH 28/39] 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 0448a859a949b9503f3886c786ea96f216f123ad Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 14 Oct 2026 18:41:09 +0200 Subject: [PATCH 29/39] 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 cc00f905cd9c262580087efb82745c219f81dec5 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 16:16:42 +0200 Subject: [PATCH 30/39] 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 2c6707ddbcdc1f97f65fc97878f543b0e4cfd046 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 16:23:05 +0200 Subject: [PATCH 31/39] 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 e329779170e1931cd6a861130724165e05f5d050 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:17:39 +0200 Subject: [PATCH 32/39] 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 6ab6d1788419b361b8bc550a8cc207e4b06c9d16 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:20:21 +0200 Subject: [PATCH 33/39] 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 | 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 b/app/build.gradle index f116ba1dd..9b0a6b144 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,6 +156,7 @@ dependencies { // Glide 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 d8a5ebbffa1d1159e82a542a773fa8b16f8994cb Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:47:52 +0200 Subject: [PATCH 34/39] 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 a4775b23fa49c36481c7ccf45e69faf83eee2421 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:50:37 +0200 Subject: [PATCH 35/39] 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 | 3 ++- build.gradle | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9b0a6b144..2107a94d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,7 @@ */ apply plugin: 'com.android.application' +apply plugin: 'com.google.devtools.ksp' android { namespace = 'it.niedermann.owncloud.notes' @@ -155,7 +156,7 @@ dependencies { // Glide implementation "com.github.bumptech.glide:glide:$glideVersion" - annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" + ksp "com.github.bumptech.glide:ksp:$glideVersion" implementation "com.caverock:androidsvg-aar:1.4" // Android X diff --git a/build.gradle b/build.gradle index 85903d40a..198e0880d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:9.4.1' 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 fa63072b365f8d6f2a8caffa88db154a7fbcc0c7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 15 Oct 2026 17:53:36 +0200 Subject: [PATCH 36/39] gradlew --write-verification-metadata Signed-off-by: daniele-verducci --- gradle/verification-metadata.xml | 2275 +++++++++++++++++++++++++++++- 1 file changed, 2242 insertions(+), 33 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 830e9a0bb..97e40397a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -15,6 +15,7 @@ + @@ -111,6 +112,7 @@ + @@ -180,6 +182,7 @@ + @@ -418,6 +421,11 @@ + + + + + @@ -544,6 +552,14 @@ + + + + + + + + @@ -597,6 +613,14 @@ + + + + + + + + @@ -621,6 +645,14 @@ + + + + + + + + @@ -705,6 +737,11 @@ + + + + + @@ -734,6 +771,14 @@ + + + + + + + + @@ -781,6 +826,14 @@ + + + + + + + + @@ -881,6 +934,11 @@ + + + + + @@ -896,6 +954,11 @@ + + + + + @@ -960,6 +1023,14 @@ + + + + + + + + @@ -1039,6 +1110,11 @@ + + + + + @@ -1103,6 +1179,14 @@ + + + + + + + + @@ -1182,6 +1266,11 @@ + + + + + @@ -1246,6 +1335,14 @@ + + + + + + + + @@ -1325,6 +1422,11 @@ + + + + + @@ -1389,6 +1491,14 @@ + + + + + + + + @@ -1479,6 +1589,14 @@ + + + + + + + + @@ -1494,6 +1612,11 @@ + + + + + @@ -1558,6 +1681,14 @@ + + + + + + + + @@ -1676,6 +1807,11 @@ + + + + + @@ -1740,6 +1876,14 @@ + + + + + + + + @@ -1819,6 +1963,11 @@ + + + + + @@ -1863,6 +2012,14 @@ + + + + + + + + @@ -1910,6 +2067,11 @@ + + + + + @@ -1934,6 +2096,14 @@ + + + + + + + + @@ -1949,6 +2119,11 @@ + + + + + @@ -2013,6 +2188,14 @@ + + + + + + + + @@ -2092,6 +2275,11 @@ + + + + + @@ -2156,6 +2344,14 @@ + + + + + + + + @@ -2235,6 +2431,11 @@ + + + + + @@ -2299,6 +2500,14 @@ + + + + + + + + @@ -2378,6 +2587,11 @@ + + + + + @@ -2442,6 +2656,14 @@ + + + + + + + + @@ -2521,6 +2743,11 @@ + + + + + @@ -2585,6 +2812,14 @@ + + + + + + + + @@ -2649,6 +2884,19 @@ + + + + + + + + + + + + + @@ -2664,6 +2912,11 @@ + + + + + @@ -2728,6 +2981,14 @@ + + + + + + + + @@ -2807,6 +3068,11 @@ + + + + + @@ -2871,6 +3137,14 @@ + + + + + + + + @@ -2988,6 +3262,14 @@ + + + + + + + + @@ -2996,6 +3278,14 @@ + + + + + + + + @@ -3075,6 +3365,14 @@ + + + + + + + + @@ -3112,6 +3410,14 @@ + + + + + + + + @@ -3161,6 +3467,9 @@ + + + @@ -3314,6 +3623,14 @@ + + + + + + + + @@ -3458,6 +3775,14 @@ + + + + + + + + @@ -3602,6 +3927,14 @@ + + + + + + + + @@ -3695,6 +4028,14 @@ + + + + + + + + @@ -3772,6 +4113,14 @@ + + + + + + + + @@ -3809,6 +4158,14 @@ + + + + + + + + @@ -3865,6 +4222,11 @@ + + + + + @@ -3931,6 +4293,14 @@ + + + + + + + + @@ -3979,6 +4349,14 @@ + + + + + + + + @@ -4031,6 +4409,9 @@ + + + @@ -4131,6 +4512,14 @@ + + + + + + + + @@ -4184,6 +4573,14 @@ + + + + + + + + @@ -4232,6 +4629,14 @@ + + + + + + + + @@ -4277,6 +4682,14 @@ + + + + + + + + @@ -4325,6 +4738,11 @@ + + + + + @@ -4383,6 +4801,14 @@ + + + + + + + + @@ -4428,6 +4854,11 @@ + + + + + @@ -4461,6 +4892,14 @@ + + + + + + + + @@ -4509,6 +4948,11 @@ + + + + + @@ -4557,6 +5001,14 @@ + + + + + + + + @@ -4605,6 +5057,14 @@ + + + + + + + + @@ -4656,6 +5116,14 @@ + + + + + + + + @@ -4734,6 +5202,14 @@ + + + + + + + + @@ -4782,6 +5258,14 @@ + + + + + + + + @@ -4830,6 +5314,11 @@ + + + + + @@ -4883,6 +5372,14 @@ + + + + + + + + @@ -4979,6 +5476,14 @@ + + + + + + + + @@ -5059,6 +5564,11 @@ + + + + + @@ -5091,6 +5601,14 @@ + + + + + + + + @@ -5131,6 +5649,14 @@ + + + + + + + + @@ -5171,6 +5697,14 @@ + + + + + + + + @@ -5203,6 +5737,14 @@ + + + + + + + + @@ -5260,6 +5802,11 @@ + + + + + @@ -5292,6 +5839,14 @@ + + + + + + + + @@ -5332,6 +5887,11 @@ + + + + + @@ -5364,6 +5924,14 @@ + + + + + + + + @@ -5838,6 +6406,19 @@ + + + + + + + + + + + + + @@ -5846,6 +6427,14 @@ + + + + + + + + @@ -6035,6 +6624,14 @@ + + + + + + + + @@ -6195,8 +6792,16 @@ - - + + + + + + + + + + @@ -6339,6 +6944,14 @@ + + + + + + + + @@ -6483,6 +7096,14 @@ + + + + + + + + @@ -6627,6 +7248,14 @@ + + + + + + + + @@ -6771,6 +7400,14 @@ + + + + + + + + @@ -6995,6 +7632,14 @@ + + + + + + + + @@ -7075,6 +7720,14 @@ + + + + + + + + @@ -7219,6 +7872,14 @@ + + + + + + + + @@ -7363,6 +8024,14 @@ + + + + + + + + @@ -7507,6 +8176,14 @@ + + + + + + + + @@ -7651,6 +8328,14 @@ + + + + + + + + @@ -7795,6 +8480,14 @@ + + + + + + + + @@ -7939,6 +8632,14 @@ + + + + + + + + @@ -8083,6 +8784,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8347,6 +9088,14 @@ + + + + + + + + @@ -8491,6 +9240,14 @@ + + + + + + + + @@ -8635,6 +9392,14 @@ + + + + + + + + @@ -8779,6 +9544,14 @@ + + + + + + + + @@ -8923,6 +9696,14 @@ + + + + + + + + @@ -9067,6 +9848,14 @@ + + + + + + + + @@ -9211,6 +10000,14 @@ + + + + + + + + @@ -9419,6 +10216,14 @@ + + + + + + + + @@ -9563,6 +10368,14 @@ + + + + + + + + @@ -9603,6 +10416,14 @@ + + + + + + + + @@ -9747,6 +10568,14 @@ + + + + + + + + @@ -9891,6 +10720,14 @@ + + + + + + + + @@ -10059,6 +10896,14 @@ + + + + + + + + @@ -10251,6 +11096,14 @@ + + + + + + + + @@ -10331,6 +11184,14 @@ + + + + + + + + @@ -10411,6 +11272,14 @@ + + + + + + + + @@ -10555,6 +11424,14 @@ + + + + + + + + @@ -10635,6 +11512,14 @@ + + + + + + + + @@ -10715,6 +11600,14 @@ + + + + + + + + @@ -10795,6 +11688,14 @@ + + + + + + + + @@ -10875,6 +11776,14 @@ + + + + + + + + @@ -11019,6 +11928,14 @@ + + + + + + + + @@ -11163,6 +12080,14 @@ + + + + + + + + @@ -13643,6 +14568,14 @@ + + + + + + + + @@ -13659,6 +14592,14 @@ + + + + + + + + @@ -13810,6 +14751,14 @@ + + + + + + + + @@ -13850,6 +14799,14 @@ + + + + + + + + @@ -13882,6 +14839,14 @@ + + + + + + + + @@ -13898,6 +14863,14 @@ + + + + + + + + @@ -13914,6 +14887,22 @@ + + + + + + + + + + + + + + + + @@ -15576,11 +16565,24 @@ + + + + + + + + + + + + + @@ -15602,6 +16604,14 @@ + + + + + + + + @@ -15623,6 +16633,11 @@ + + + + + @@ -15686,6 +16701,14 @@ + + + + + + + + @@ -15727,6 +16750,11 @@ + + + + + @@ -15825,6 +16853,19 @@ + + + + + + + + + + + + + @@ -15841,6 +16882,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -15992,6 +17065,14 @@ + + + + + + + + @@ -16082,6 +17163,11 @@ + + + + + @@ -16159,6 +17245,14 @@ + + + + + + + + @@ -16195,6 +17289,9 @@ + + + @@ -16599,6 +17696,14 @@ + + + + + + + + @@ -16647,6 +17752,14 @@ + + + + + + + + @@ -16705,6 +17818,14 @@ + + + + + + + + @@ -16750,6 +17871,11 @@ + + + + + @@ -16766,6 +17892,19 @@ + + + + + + + + + + + + + @@ -16800,6 +17939,11 @@ + + + + + @@ -16837,6 +17981,14 @@ + + + + + + + + @@ -16971,6 +18123,11 @@ + + + + + @@ -16987,6 +18144,14 @@ + + + + + + + + @@ -17003,6 +18168,11 @@ + + + + + @@ -17019,6 +18189,14 @@ + + + + + + + + @@ -17130,53 +18308,144 @@ - - - + + + - - + + + + + + + - - - + + + - - - + + + - - + + - - - - - - + + + - - - + + + - - + + - - - + + + - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -18258,6 +19527,19 @@ + + + + + + + + + + + + + @@ -18336,6 +19618,11 @@ + + + + + @@ -18376,6 +19663,14 @@ + + + + + + + + @@ -18392,6 +19687,14 @@ + + + + + + + + @@ -18432,6 +19735,14 @@ + + + + + + + + @@ -18448,6 +19759,14 @@ + + + + + + + + @@ -18480,6 +19799,14 @@ + + + + + + + + @@ -18496,6 +19823,14 @@ + + + + + + + + @@ -18607,6 +19942,14 @@ + + + + + + + + @@ -18676,6 +20019,14 @@ + + + + + + + + @@ -18703,6 +20054,14 @@ + + + + + + + + @@ -18752,6 +20111,14 @@ + + + + + + + + @@ -18776,6 +20143,14 @@ + + + + + + + + @@ -18832,6 +20207,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -18968,6 +20524,14 @@ + + + + + + + + @@ -19098,6 +20662,14 @@ + + + + + + + + @@ -19162,6 +20734,22 @@ + + + + + + + + + + + + + + + + @@ -19178,6 +20766,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -19250,6 +20862,22 @@ + + + + + + + + + + + + + + + + @@ -19314,6 +20942,22 @@ + + + + + + + + + + + + + + + + @@ -19378,6 +21022,22 @@ + + + + + + + + + + + + + + + + @@ -19442,6 +21102,22 @@ + + + + + + + + + + + + + + + + @@ -19506,6 +21182,22 @@ + + + + + + + + + + + + + + + + @@ -19570,6 +21262,14 @@ + + + + + + + + @@ -19634,6 +21334,14 @@ + + + + + + + + @@ -19710,6 +21418,14 @@ + + + + + + + + @@ -19774,6 +21490,14 @@ + + + + + + + + @@ -19838,6 +21562,14 @@ + + + + + + + + @@ -19955,6 +21687,14 @@ + + + + + + + + @@ -20035,6 +21775,14 @@ + + + + + + + + @@ -20155,6 +21903,14 @@ + + + + + + + + @@ -20219,6 +21975,14 @@ + + + + + + + + @@ -20280,6 +22044,14 @@ + + + + + + + + @@ -20288,6 +22060,14 @@ + + + + + + + + @@ -20424,6 +22204,22 @@ + + + + + + + + + + + + + + + + @@ -20775,6 +22571,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -20872,6 +22692,19 @@ + + + + + + + + + + + + + @@ -20984,6 +22817,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -21101,6 +22955,14 @@ + + + + + + + + @@ -21165,6 +23027,14 @@ + + + + + + + + @@ -21229,6 +23099,14 @@ + + + + + + + + @@ -21293,6 +23171,14 @@ + + + + + + + + @@ -21357,6 +23243,14 @@ + + + + + + + + @@ -21423,6 +23317,19 @@ + + + + + + + + + + + + + @@ -21447,6 +23354,11 @@ + + + + + @@ -21472,6 +23384,11 @@ + + + + + @@ -21515,6 +23432,14 @@ + + + + + + + + @@ -21563,6 +23488,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -21578,6 +23542,11 @@ + + + + + @@ -21596,6 +23565,14 @@ + + + + + + + + @@ -21620,6 +23597,11 @@ + + + + + @@ -21635,6 +23617,14 @@ + + + + + + + + @@ -21691,6 +23681,14 @@ + + + + + + + + @@ -21706,6 +23704,11 @@ + + + + + @@ -21719,6 +23722,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -21775,6 +23807,14 @@ + + + + + + + + @@ -21788,6 +23828,14 @@ + + + + + + + + @@ -21799,6 +23847,14 @@ + + + + + + + + @@ -21832,6 +23888,9 @@ + + + @@ -21868,6 +23927,14 @@ + + + + + + + + @@ -21888,6 +23955,14 @@ + + + + + + + + @@ -21921,10 +23996,21 @@ + + + + + + + + + + + @@ -21958,6 +24044,9 @@ + + + @@ -21986,6 +24075,14 @@ + + + + + + + + @@ -22034,6 +24131,14 @@ + + + + + + + + @@ -22066,6 +24171,14 @@ + + + + + + + + @@ -22098,6 +24211,14 @@ + + + + + + + + @@ -22122,6 +24243,14 @@ + + + + + + + + @@ -22154,6 +24283,14 @@ + + + + + + + + @@ -22186,6 +24323,14 @@ + + + + + + + + @@ -22218,6 +24363,14 @@ + + + + + + + + @@ -22250,6 +24403,14 @@ + + + + + + + + @@ -22282,6 +24443,14 @@ + + + + + + + + @@ -22314,6 +24483,14 @@ + + + + + + + + @@ -22346,6 +24523,14 @@ + + + + + + + + @@ -22378,6 +24563,14 @@ + + + + + + + + @@ -22410,6 +24603,14 @@ + + + + + + + + @@ -22529,6 +24730,14 @@ + + + + + + + + From 657b0e450bc5295f039bc15636fe33ad0a362474 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 14:34:29 +0200 Subject: [PATCH 37/39] 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 | 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 b/app/build.gradle index 2107a94d8..fde0045fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.0") { 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 547ee2040eb06033aa5784b837bf11bb0ee36486 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 15:02:58 +0200 Subject: [PATCH 38/39] 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 6bcf17bd49fc34f3a68b59b056ca2abbe1f1e7a2 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 16 Oct 2026 15:50:26 +0200 Subject: [PATCH 39/39] 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 } }