From c85964ae791a858fb5a823d03505f180a36c83e4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 14:56:06 +0200 Subject: [PATCH 01/18] Rename .java to .kt Signed-off-by: alperozturk96 --- .../{NotificationListAdapter.java => NotificationListAdapter.kt} | 0 ...ionExecuteActionTask.java => NotificationExecuteActionTask.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/adapter/{NotificationListAdapter.java => NotificationListAdapter.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/asynctasks/{NotificationExecuteActionTask.java => NotificationExecuteActionTask.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java rename to app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.java rename to app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt From b56e48525acced8abedc80089c0c5ebd30de463d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 14:56:07 +0200 Subject: [PATCH 02/18] fix(navigator): notifications Signed-off-by: alperozturk96 --- app/src/main/AndroidManifest.xml | 3 - .../nextcloud/client/di/ComponentsModule.java | 8 +- .../nextcloud/client/jobs/NotificationWork.kt | 4 +- .../android/ui/activity/DrawerActivity.java | 4 +- .../ui/activity/FileDisplayActivity.kt | 5 +- .../ui/adapter/NotificationListAdapter.kt | 532 +++++++++--------- .../DeleteAllNotificationsTask.java | 40 -- .../ui/asynctasks/DeleteNotificationTask.java | 53 -- .../NotificationExecuteActionTask.kt | 118 ++-- .../notifications/NotificationsFragment.kt} | 320 +++++------ .../ui/navigation/NavigatorActivity.kt | 30 +- .../android/ui/navigation/NavigatorScreen.kt | 11 +- 12 files changed, 525 insertions(+), 603 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/asynctasks/DeleteAllNotificationsTask.java delete mode 100644 app/src/main/java/com/owncloud/android/ui/asynctasks/DeleteNotificationTask.java rename app/src/main/java/com/owncloud/android/ui/{activity/NotificationsActivity.kt => fragment/notifications/NotificationsFragment.kt} (51%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7be81f621b06..a500a6b8f596 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -282,9 +282,6 @@ - diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index c2e55a52afa1..9a0b4193d292 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -61,7 +61,6 @@ import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity; import com.owncloud.android.ui.activity.ManageAccountsActivity; import com.owncloud.android.ui.activity.ManageSpaceActivity; -import com.owncloud.android.ui.activity.NotificationsActivity; import com.owncloud.android.ui.activity.PassCodeActivity; import com.owncloud.android.ui.activity.ReceiveExternalFilesActivity; import com.owncloud.android.ui.activity.RequestCredentialsActivity; @@ -118,6 +117,7 @@ import com.owncloud.android.ui.fragment.community.CommunityFragment; import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment; import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment; +import com.owncloud.android.ui.fragment.notifications.NotificationsFragment; import com.owncloud.android.ui.navigation.NavigatorActivity; import com.owncloud.android.ui.preview.FileDownloadFragment; import com.owncloud.android.ui.preview.PreviewBitmapActivity; @@ -144,6 +144,9 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract ActivitiesFragment activitiesFragment(); + @ContributesAndroidInjector + abstract NotificationsFragment notificationFragment(); + @ContributesAndroidInjector abstract AuthenticatorActivity authenticatorActivity(); @@ -192,9 +195,6 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract ManageSpaceActivity manageSpaceActivity(); - @ContributesAndroidInjector - abstract NotificationsActivity notificationsActivity(); - @ContributesAndroidInjector abstract ComposeActivity composeActivity(); diff --git a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt index f95e110ff491..b852fea8176e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt @@ -40,7 +40,7 @@ import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemote import com.owncloud.android.lib.resources.notifications.GetNotificationRemoteOperation import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.activity.NotificationsActivity +import com.owncloud.android.ui.navigation.NavigatorActivity import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.PushUtils import com.owncloud.android.utils.theme.ViewThemeUtils @@ -137,7 +137,7 @@ class NotificationWork constructor( } else { val intent: Intent if (file == null) { - intent = Intent(context, NotificationsActivity::class.java) + intent = Intent(context, NavigatorActivity::class.java) } else { intent = Intent(context, FileDisplayActivity::class.java) intent.action = Intent.ACTION_VIEW diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index de80beee66e4..b63241173040 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -655,7 +655,7 @@ private void onNavigationItemClicked(final MenuItem menuItem) { /** * If navigator activity already exists just push else start navigator activity. */ - private void pushFragment(NavigatorScreen screen) { + public void pushFragment(NavigatorScreen screen) { if (this instanceof NavigatorActivity navigatorActivity) { navigatorActivity.push(screen); } else { @@ -1449,7 +1449,7 @@ protected void handleDeepLink(@NonNull Uri uri) { LinkHelper.INSTANCE.openAppStore(getPackageName(), false, this); break; case OPEN_NOTIFICATIONS: - startActivity(NotificationsActivity.class); + pushFragment(NavigatorScreen.Notifications.INSTANCE); break; default: handleNavItemClickEvent(deepLinkType.getNavId()); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index f4c9becd00b6..382590f6a945 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -139,6 +139,7 @@ import com.owncloud.android.ui.fragment.UnifiedSearchFragment import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.ui.interfaces.TransactionInterface +import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.ui.preview.PreviewImageActivity import com.owncloud.android.ui.preview.PreviewImageFragment import com.owncloud.android.ui.preview.PreviewMediaActivity @@ -334,7 +335,9 @@ class FileDisplayActivity : setupHomeSearchToolbarWithSortAndListButtons() mMenuButton.setOnClickListener { v: View? -> openDrawer() } mSwitchAccountButton.setOnClickListener { v: View? -> showManageAccountsDialog() } - mNotificationButton.setOnClickListener { v: View? -> startActivity(NotificationsActivity::class.java) } + mNotificationButton.setOnClickListener { + pushFragment(NavigatorScreen.Notifications) + } fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout) // reset ui states when file display activity created/recrated diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 67ca8c616846..7163707150fe 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -8,364 +8,372 @@ * SPDX-FileCopyrightText: 2017 Mario Danic * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.adapter; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import com.google.android.material.button.MaterialButton; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.utils.GlideHelper; -import com.owncloud.android.R; -import com.owncloud.android.databinding.NotificationListItemBinding; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.notifications.models.Action; -import com.owncloud.android.lib.resources.notifications.models.Notification; -import com.owncloud.android.lib.resources.notifications.models.RichObject; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.activity.NotificationsActivity; -import com.owncloud.android.ui.asynctasks.DeleteNotificationTask; -import com.owncloud.android.ui.asynctasks.NotificationExecuteActionTask; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.res.ResourcesCompat; -import androidx.recyclerview.widget.RecyclerView; +package com.owncloud.android.ui.adapter + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.res.Resources +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.net.toUri +import androidx.core.view.size +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.GlideHelper +import com.owncloud.android.R +import com.owncloud.android.databinding.NotificationListItemBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation +import com.owncloud.android.lib.resources.notifications.models.Action +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.asynctasks.NotificationExecuteActionTask +import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * This Adapter populates a RecyclerView with all notifications for an account within the app. */ -public class NotificationListAdapter extends RecyclerView.Adapter { - private static final String FILE = "file"; - private static final String ACTION_TYPE_WEB = "WEB"; - private final StyleSpan styleSpanBold = new StyleSpan(Typeface.BOLD); - private final ForegroundColorSpan foregroundColorSpanBlack; - - private final List notificationsList; - private final NextcloudClient client; - private final NotificationsActivity notificationsActivity; - private final ViewThemeUtils viewThemeUtils; - - public NotificationListAdapter(NextcloudClient client, - NotificationsActivity notificationsActivity, - ViewThemeUtils viewThemeUtils) { - this.notificationsList = new ArrayList<>(); - this.client = client; - this.notificationsActivity = notificationsActivity; - this.viewThemeUtils = viewThemeUtils; - foregroundColorSpanBlack = new ForegroundColorSpan( - notificationsActivity.getResources().getColor(R.color.text_color)); - } +class NotificationListAdapter( + private val client: NextcloudClient?, + private val fragment: NotificationsFragment, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter() { + private val styleSpanBold = StyleSpan(Typeface.BOLD) + private val foregroundColorSpanBlack: ForegroundColorSpan = ForegroundColorSpan( + ContextCompat.getColor(fragment.requireContext(), R.color.text_color) + ) + + private val notificationsList: ArrayList = ArrayList() @SuppressLint("NotifyDataSetChanged") - public void setNotificationItems(List notificationItems) { - notificationsList.clear(); - notificationsList.addAll(notificationItems); - notifyDataSetChanged(); + fun setNotificationItems(notificationItems: List) { + notificationsList.clear() + notificationsList.addAll(notificationItems) + notifyDataSetChanged() } - @NonNull - @Override - public NotificationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new NotificationViewHolder( - NotificationListItemBinding.inflate(LayoutInflater.from(notificationsActivity)) - ); + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder { + return NotificationViewHolder( + NotificationListItemBinding.inflate(LayoutInflater.from(fragment.requireContext())) + ) } - @Override - public void onBindViewHolder(@NonNull NotificationViewHolder holder, int position) { - Notification notification = notificationsList.get(position); - holder.binding.datetime.setText(DisplayUtils.getRelativeTimestamp(notificationsActivity, - notification.getDatetime().getTime())); + override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { + val notification = notificationsList[position] + holder.binding.datetime.text = DisplayUtils.getRelativeTimestamp( + fragment.requireContext(), + notification.getDatetime().time + ) - RichObject file = notification.subjectRichParameters.get(FILE); - String subject = notification.getSubject(); + val file = notification.subjectRichParameters[FILE] + var subject = notification.getSubject() if (file == null && !TextUtils.isEmpty(notification.getLink())) { - subject = subject + " ↗"; - holder.binding.subject.setTypeface(holder.binding.subject.getTypeface(), - Typeface.BOLD); - holder.binding.subject.setOnClickListener(v -> DisplayUtils.startLinkIntent(notificationsActivity, - notification.getLink())); - holder.binding.subject.setText(subject); + subject = "$subject ↗" + holder.binding.subject.setTypeface( + holder.binding.subject.typeface, + Typeface.BOLD + ) + holder.binding.subject.setOnClickListener { + DisplayUtils.startLinkIntent( + fragment.requireActivity(), + notification.getLink() + ) + } + holder.binding.subject.text = subject } else { if (!TextUtils.isEmpty(notification.subjectRich)) { - holder.binding.subject.setText(makeSpecialPartsBold(notification)); + holder.binding.subject.text = makeSpecialPartsBold(notification) } else { - holder.binding.subject.setText(subject); + holder.binding.subject.text = subject } if (file != null && !TextUtils.isEmpty(file.id)) { - holder.binding.subject.setOnClickListener(v -> { - Intent intent = new Intent(notificationsActivity, FileDisplayActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id); - - notificationsActivity.startActivity(intent); - }); + holder.binding.subject.setOnClickListener { + val intent = Intent(fragment.requireActivity(), FileDisplayActivity::class.java) + intent.setAction(Intent.ACTION_VIEW) + intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id) + fragment.requireActivity().startActivity(intent) + } } } if (notification.getMessage() != null && !notification.getMessage().isEmpty()) { - holder.binding.message.setText(notification.getMessage()); - holder.binding.message.setVisibility(View.VISIBLE); + holder.binding.message.text = notification.getMessage() + holder.binding.message.visibility = View.VISIBLE } else { - holder.binding.message.setVisibility(View.GONE); + holder.binding.message.visibility = View.GONE } if (!TextUtils.isEmpty(notification.getIcon())) { - new Thread(() -> { - { - try { - notificationsActivity.runOnUiThread(() -> GlideHelper.INSTANCE - .loadIntoImageView(notificationsActivity, - client, - notification.getIcon(), - holder.binding.icon, - R.drawable.ic_notification, - false)); - } catch (Exception e) { - Log_OC.e("RichDocumentsTemplateAdapter", "Exception setData: " + e); + fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + withContext(Dispatchers.Main) { + GlideHelper + .loadIntoImageView( + fragment.requireContext(), + client, + notification.getIcon(), + holder.binding.icon, + R.drawable.ic_notification, + false + ) } + } catch (e: Exception) { + Log_OC.e("RichDocumentsTemplateAdapter", "Exception setData: " + e) } - }).start(); + } + } + + viewThemeUtils.platform.run { + colorImageView(holder.binding.icon, ColorRole.ON_SURFACE_VARIANT) + colorImageView(holder.binding.dismiss, ColorRole.ON_SURFACE_VARIANT) + colorTextView(holder.binding.subject, ColorRole.ON_SURFACE) + colorTextView(holder.binding.message, ColorRole.ON_SURFACE_VARIANT) + colorTextView(holder.binding.datetime, ColorRole.ON_SURFACE_VARIANT) } - viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.ON_SURFACE_VARIANT); - viewThemeUtils.platform.colorImageView(holder.binding.dismiss, ColorRole.ON_SURFACE_VARIANT); - viewThemeUtils.platform.colorTextView(holder.binding.subject, ColorRole.ON_SURFACE); - viewThemeUtils.platform.colorTextView(holder.binding.message, ColorRole.ON_SURFACE_VARIANT); - viewThemeUtils.platform.colorTextView(holder.binding.datetime, ColorRole.ON_SURFACE_VARIANT); - setButtons(holder, notification); + setButtons(holder, notification) - holder.binding.dismiss.setOnClickListener(v -> new DeleteNotificationTask(client, - notification, - holder, - notificationsActivity).execute()); + holder.binding.dismiss.setOnClickListener { + fragment.lifecycleScope.launch(Dispatchers.IO) { + val result = DeleteNotificationRemoteOperation(notification.notificationId) + .execute(client!!) + withContext(Dispatchers.Main) { + fragment.onRemovedNotification(result.isSuccess) + } + } + } + } + + override fun getItemCount(): Int { + return notificationsList.size } - public void setButtons(NotificationViewHolder holder, Notification notification) { + fun setButtons(holder: NotificationViewHolder, notification: Notification) { // add action buttons - holder.binding.buttons.removeAllViews(); + holder.binding.buttons.removeAllViews() - Resources resources = notificationsActivity.getResources(); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + val resources: Resources = fragment.resources + val params: LinearLayout.LayoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) params.setMargins( resources.getDimensionPixelOffset(R.dimen.standard_quarter_margin), 0, resources.getDimensionPixelOffset(R.dimen.standard_half_margin), - 0); + 0 + ) - List overflowActions = new ArrayList<>(); + val overflowActions = ArrayList() - if (notification.getActions().size() > 0) { - holder.binding.buttons.setVisibility(View.VISIBLE); + if (notification.getActions().isNotEmpty()) { + holder.binding.buttons.visibility = View.VISIBLE } else { - holder.binding.buttons.setVisibility(View.GONE); + holder.binding.buttons.visibility = View.GONE } - if (notification.getActions().size() > 2) { - for (Action action : notification.getActions()) { + if (notification.getActions().size > 2) { + for (action in notification.getActions()) { if (action.primary) { - final MaterialButton button = new MaterialButton(notificationsActivity); - button.setAllCaps(false); - - button.setText(action.label); - button.setCornerRadiusResource(R.dimen.button_corner_radius); + val button: MaterialButton = MaterialButton(fragment.requireContext()) + button.setAllCaps(false) - button.setLayoutParams(params); - button.setGravity(Gravity.CENTER); + button.text = action.label + button.setCornerRadiusResource(R.dimen.button_corner_radius) - button.setOnClickListener(v -> { - setButtonEnabled(holder, false); + button.setLayoutParams(params) + button.setGravity(Gravity.CENTER) - if (ACTION_TYPE_WEB.equals(action.type)) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(action.link)); - - notificationsActivity.startActivity(intent); + button.setOnClickListener { + setButtonEnabled(holder, false) + if (ACTION_TYPE_WEB == action.type) { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(action.link?.toUri()) + fragment.requireActivity().startActivity(intent) } else { - new NotificationExecuteActionTask(client, - holder, - notification, - notificationsActivity) - .execute(action); + NotificationExecuteActionTask( + client!!, + holder, + notification, + fragment + ) + .execute(action) } - }); + } - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button); - holder.binding.buttons.addView(button); + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) + holder.binding.buttons.addView(button) } else { - overflowActions.add(action); + overflowActions.add(action) } } // further actions - final MaterialButton moreButton = new MaterialButton(notificationsActivity); - moreButton.setBackgroundColor(ResourcesCompat.getColor(resources, - android.R.color.transparent, - null)); - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(moreButton); - - moreButton.setAllCaps(false); - - moreButton.setText(R.string.more); - moreButton.setCornerRadiusResource(R.dimen.button_corner_radius); - - moreButton.setLayoutParams(params); - moreButton.setGravity(Gravity.CENTER); - - moreButton.setOnClickListener(v -> { - PopupMenu popup = new PopupMenu(notificationsActivity, moreButton); - - for (Action action : overflowActions) { - popup.getMenu().add(action.label).setOnMenuItemClickListener(item -> { - setButtonEnabled(holder, false); - - if (ACTION_TYPE_WEB.equals(action.type)) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(action.link)); - - notificationsActivity.startActivity(intent); - } else { - new NotificationExecuteActionTask(client, - holder, - notification, - notificationsActivity) - .execute(action); + val moreButton = MaterialButton(fragment.requireContext()) + moreButton.setBackgroundColor( + ResourcesCompat.getColor( + resources, + android.R.color.transparent, + null + ) + ) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(moreButton) + + moreButton.setAllCaps(false) + moreButton.setText(R.string.more) + moreButton.setCornerRadiusResource(R.dimen.button_corner_radius) + moreButton.setLayoutParams(params) + moreButton.setGravity(Gravity.CENTER) + moreButton.setOnClickListener { + val popup = PopupMenu(fragment.requireContext(), moreButton) + for (action in overflowActions) { + popup.menu.add(action.label) + .setOnMenuItemClickListener { + setButtonEnabled(holder, false) + if (ACTION_TYPE_WEB == action.type) { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(action.link?.toUri()) + fragment.requireActivity().startActivity(intent) + } else { + NotificationExecuteActionTask( + client!!, + holder, + notification, + fragment + ) + .execute(action) + } + true } - - return true; - }); } + popup.show() + } - popup.show(); - }); - - holder.binding.buttons.addView(moreButton); + holder.binding.buttons.addView(moreButton) } else { - for (Action action : notification.getActions()) { - final MaterialButton button = new MaterialButton(notificationsActivity); + for (action in notification.getActions()) { + val button = MaterialButton(fragment.requireContext()) if (action.primary) { - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button); + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) } else { - button.setBackgroundColor(ResourcesCompat.getColor(resources, - android.R.color.transparent, - null)); - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(button); + button.setBackgroundColor( + ResourcesCompat.getColor( + resources, + android.R.color.transparent, + null + ) + ) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(button) } - button.setAllCaps(false); - - button.setText(action.label); - button.setCornerRadiusResource(R.dimen.button_corner_radius); + button.setAllCaps(false) - button.setLayoutParams(params); + button.text = action.label + button.setCornerRadiusResource(R.dimen.button_corner_radius) - button.setOnClickListener(v -> { - setButtonEnabled(holder, false); + button.setLayoutParams(params) - if (ACTION_TYPE_WEB.equals(action.type)) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(action.link)); - - notificationsActivity.startActivity(intent); + button.setOnClickListener { + setButtonEnabled(holder, false) + if (ACTION_TYPE_WEB == action.type) { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(action.link?.toUri()) + fragment.requireActivity().startActivity(intent) } else { - new NotificationExecuteActionTask(client, - holder, - notification, - notificationsActivity) - .execute(action); + NotificationExecuteActionTask( + client!!, + holder, + notification, + fragment + ) + .execute(action) } - }); + } - holder.binding.buttons.addView(button); + holder.binding.buttons.addView(button) } } } - private SpannableStringBuilder makeSpecialPartsBold(Notification notification) { - String text = notification.getSubjectRich(); - SpannableStringBuilder ssb = new SpannableStringBuilder(text); + private fun makeSpecialPartsBold(notification: Notification): SpannableStringBuilder { + var text = notification.getSubjectRich() + val ssb = SpannableStringBuilder(text) - int openingBrace = text.indexOf('{'); - int closingBrace; - String replaceablePart; + var openingBrace = text.indexOf('{') + var closingBrace: Int + var replaceablePart: String? while (openingBrace != -1) { - closingBrace = text.indexOf('}', openingBrace) + 1; - replaceablePart = text.substring(openingBrace + 1, closingBrace - 1); + closingBrace = text.indexOf('}', openingBrace) + 1 + replaceablePart = text.substring(openingBrace + 1, closingBrace - 1) - RichObject richObject = notification.subjectRichParameters.get(replaceablePart); + val richObject = notification.subjectRichParameters.get(replaceablePart) if (richObject != null) { - String name = richObject.getName(); - ssb.replace(openingBrace, closingBrace, name); - text = ssb.toString(); - closingBrace = openingBrace + name.length(); + val name = richObject.name + ssb.replace(openingBrace, closingBrace, name) + text = ssb.toString() + closingBrace = openingBrace + name!!.length - ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0); - ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0) + ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } - openingBrace = text.indexOf('{', closingBrace); + openingBrace = text.indexOf('{', closingBrace) } - return ssb; + return ssb } - public void removeNotification(NotificationViewHolder holder) { - int position = holder.getAdapterPosition(); + fun removeNotification(holder: NotificationViewHolder) { + val position = holder.bindingAdapterPosition - if (position >= 0 && position < notificationsList.size()) { - notificationsList.remove(position); - notifyItemRemoved(position); - notifyItemRangeChanged(position, notificationsList.size()); + if (position >= 0 && position < notificationsList.size) { + notificationsList.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, notificationsList.size) } } @SuppressLint("NotifyDataSetChanged") - public void removeAllNotifications() { - notificationsList.clear(); - notifyDataSetChanged(); + fun removeAllNotifications() { + notificationsList.clear() + notifyDataSetChanged() } - - public void setButtonEnabled(NotificationViewHolder holder, boolean enabled) { - for (int i = 0; i < holder.binding.buttons.getChildCount(); i++) { - holder.binding.buttons.getChildAt(i).setEnabled(enabled); + fun setButtonEnabled(holder: NotificationViewHolder, enabled: Boolean) { + for (i in 0.. - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.asynctasks; - -import android.os.AsyncTask; - -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.resources.notifications.DeleteAllNotificationsRemoteOperation; -import com.owncloud.android.lib.resources.notifications.models.Action; -import com.owncloud.android.ui.activity.NotificationsActivity; -import com.owncloud.android.ui.notifications.NotificationsContract; - -public class DeleteAllNotificationsTask extends AsyncTask { - private NextcloudClient client; - private final NotificationsContract.View notificationsActivity; - - public DeleteAllNotificationsTask(NextcloudClient client, NotificationsActivity notificationsActivity) { - this.client = client; - this.notificationsActivity = notificationsActivity; - } - - @Override - protected Boolean doInBackground(Action... actions) { - - RemoteOperationResult result = new DeleteAllNotificationsRemoteOperation().execute(client); - - return result.isSuccess(); - } - - @Override - protected void onPostExecute(Boolean success) { - notificationsActivity.onRemovedAllNotifications(success); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/DeleteNotificationTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/DeleteNotificationTask.java deleted file mode 100644 index 458e642b4902..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/DeleteNotificationTask.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.asynctasks; - -import android.os.AsyncTask; - -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation; -import com.owncloud.android.lib.resources.notifications.models.Action; -import com.owncloud.android.lib.resources.notifications.models.Notification; -import com.owncloud.android.ui.activity.NotificationsActivity; -import com.owncloud.android.ui.adapter.NotificationListAdapter; -import com.owncloud.android.ui.notifications.NotificationsContract; - -public class DeleteNotificationTask extends AsyncTask { - private Notification notification; - private NotificationListAdapter.NotificationViewHolder holder; - private NextcloudClient client; - private NotificationsContract.View notificationsActivity; - - public DeleteNotificationTask(NextcloudClient client, Notification notification, - NotificationListAdapter.NotificationViewHolder holder, - NotificationsActivity notificationsActivity) { - this.client = client; - this.notification = notification; - this.holder = holder; - this.notificationsActivity = notificationsActivity; - } - - @Override - protected void onPreExecute() { - notificationsActivity.removeNotification(holder); - } - - @Override - protected Boolean doInBackground(Action... actions) { - RemoteOperationResult result = new DeleteNotificationRemoteOperation(notification.notificationId) - .execute(client); - - return result.isSuccess(); - } - - @Override - protected void onPostExecute(Boolean success) { - notificationsActivity.onRemovedNotification(success); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt index d8e92df6e0fe..38f4706f906c 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.kt @@ -1,87 +1,77 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.asynctasks; -import android.annotation.SuppressLint; -import android.os.AsyncTask; +package com.owncloud.android.ui.asynctasks -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.common.OkHttpMethodBase; -import com.nextcloud.operations.DeleteMethod; -import com.nextcloud.operations.GetMethod; -import com.nextcloud.operations.PostMethod; -import com.nextcloud.operations.PutMethod; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.notifications.models.Action; -import com.owncloud.android.lib.resources.notifications.models.Notification; -import com.owncloud.android.ui.activity.NotificationsActivity; -import com.owncloud.android.ui.adapter.NotificationListAdapter; +import androidx.lifecycle.lifecycleScope +import com.nextcloud.common.NextcloudClient +import com.nextcloud.common.OkHttpMethodBase +import com.nextcloud.operations.DeleteMethod +import com.nextcloud.operations.GetMethod +import com.nextcloud.operations.PostMethod +import com.nextcloud.operations.PutMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.notifications.models.Action +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.adapter.NotificationListAdapter +import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.RequestBody +import org.apache.commons.httpclient.HttpStatus +import java.io.IOException -import org.apache.commons.httpclient.HttpStatus; +class NotificationExecuteActionTask( + private val client: NextcloudClient, + private val holder: NotificationListAdapter.NotificationViewHolder, + private val notification: Notification, + private val fragment: NotificationsFragment +) { -import java.io.IOException; - -import okhttp3.RequestBody; - -public class NotificationExecuteActionTask extends AsyncTask { - - private final NotificationListAdapter.NotificationViewHolder holder; - private final NextcloudClient client; - private final Notification notification; - @SuppressLint("StaticFieldLeak") private final NotificationsActivity notificationsActivity; + fun execute(action: Action) { + fragment.lifecycleScope.launch { + val isSuccess = withContext(Dispatchers.IO) { + performRequest(action) + } - public NotificationExecuteActionTask(NextcloudClient client, - NotificationListAdapter.NotificationViewHolder holder, - Notification notification, - NotificationsActivity notificationsActivity) { - this.client = client; - this.holder = holder; - this.notification = notification; - this.notificationsActivity = notificationsActivity; + fragment.onActionCallback(isSuccess, notification, holder) + } } - @Override - protected Boolean doInBackground(Action... actions) { - OkHttpMethodBase method; - Action action = actions[0]; - + private fun performRequest(action: Action): Boolean { if (action.type == null || action.link == null) { - return Boolean.FALSE; + return false } - switch (action.type) { - case "GET" -> method = new GetMethod(action.link, true); - case "POST" -> method = new PostMethod(action.link, true, RequestBody.create("", null)); - case "DELETE" -> method = new DeleteMethod(action.link, true); - case "PUT" -> method = new PutMethod(action.link, true, null); - default -> { - // do nothing - return Boolean.FALSE; - } + val link = action.link ?: return false + val method: OkHttpMethodBase = when (action.type) { + "GET" -> GetMethod(link, true) + "POST" -> PostMethod(link, true, RequestBody.create(null, "")) + "DELETE" -> DeleteMethod(link, true) + "PUT" -> PutMethod(link, true, null) + else -> return false } - method.addRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); + method.addRequestHeader( + RemoteOperation.OCS_API_HEADER, + RemoteOperation.OCS_API_HEADER_VALUE + ) - int status; - try { - status = client.execute(method); - } catch (IOException e) { - Log_OC.e(this, "Execution of notification action failed: " + e); - return Boolean.FALSE; + return try { + val status = client.execute(method) + status == HttpStatus.SC_OK || status == HttpStatus.SC_ACCEPTED + } catch (e: IOException) { + Log_OC.e(this, "Execution of notification action failed: $e") + false } finally { - method.releaseConnection(); + method.releaseConnection() } - - return status == HttpStatus.SC_OK || status == HttpStatus.SC_ACCEPTED; - } - - @Override - protected void onPostExecute(Boolean isSuccess) { - notificationsActivity.onActionCallback(isSuccess, notification, holder); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt similarity index 51% rename from app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt rename to app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 15c1b584c77a..90332fec0dfa 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -8,42 +8,47 @@ * SPDX-FileCopyrightText: 2025 TSI-mc * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activity -import android.os.Build +package com.owncloud.android.ui.fragment.notifications + import android.os.Bundle +import android.view.LayoutInflater import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.WindowInsetsController +import android.view.ViewGroup import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar -import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.NotificationWork import com.nextcloud.client.network.ClientFactory -import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient -import com.nextcloud.utils.BuildHelper.isFlavourGPlay +import com.nextcloud.utils.BuildHelper import com.owncloud.android.R import com.owncloud.android.databinding.NotificationsLayoutBinding -import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.notifications.DeleteAllNotificationsRemoteOperation import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.adapter.NotificationListAdapter -import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder -import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask import com.owncloud.android.ui.notifications.NotificationsContract import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PushUtils import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Optional import javax.inject.Inject @@ -51,12 +56,12 @@ import javax.inject.Inject * Activity displaying all server side stored notification items. */ @Suppress("TooManyFunctions") -class NotificationsActivity : - AppCompatActivity(), +class NotificationsFragment : + Fragment(), NotificationsContract.View, Injectable { - lateinit var binding: NotificationsLayoutBinding + private var binding: NotificationsLayoutBinding? = null private var adapter: NotificationListAdapter? = null private var snackbar: Snackbar? = null @@ -75,16 +80,21 @@ class NotificationsActivity : @Inject lateinit var preferences: AppPreferences - override fun onCreate(savedInstanceState: Bundle?) { - Log_OC.v(TAG, "onCreate() start") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = NotificationsLayoutBinding.inflate(inflater, container, false) + val binding = binding!! + return binding.root + } - applyEdgeToEdgeWithSystemBarPadding() - super.onCreate(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Log_OC.v(TAG, "onViewCreated() start") - binding = NotificationsLayoutBinding.inflate(layoutInflater) - setContentView(binding.root) - setupActionBar() - setupStatusBar() + setupMenu() initUser() setupContainingList() setupPushWarning() @@ -97,57 +107,27 @@ class NotificationsActivity : private fun initUser() { optionalUser = Optional.of(accountManager.user) - intent?.let { - it.extras?.let { bundle -> - setupUser(bundle) - } + arguments?.let { bundle -> + setupUser(bundle) } } - private fun setupActionBar() { - setSupportActionBar(findViewById(R.id.toolbar_back_button)) - supportActionBar?.apply { - setTitle(R.string.drawer_item_notifications) - setDisplayHomeAsUpEnabled(true) - setHomeAsUpIndicator(R.drawable.ic_arrow_back_foreground) - } - } - - private fun setupStatusBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val appearanceLightStatusBars = if (preferences.isDarkModeEnabled) { - 0 - } else { - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + private fun setupContainingList() { + binding?.run { + viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingList) + viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingEmpty) + swipeContainingList.setOnRefreshListener { + setLoadingMessage() + swipeContainingList.isRefreshing = true + fetchAndSetData() } - window.insetsController?.setSystemBarsAppearance( - appearanceLightStatusBars, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - ) - } else { - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = if (preferences.isDarkModeEnabled) { - 0 - } else { - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + swipeContainingEmpty.setOnRefreshListener { + setLoadingMessageEmpty() + fetchAndSetData() } } } - private fun setupContainingList() { - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty) - binding.swipeContainingList.setOnRefreshListener { - setLoadingMessage() - binding.swipeContainingList.isRefreshing = true - fetchAndSetData() - } - binding.swipeContainingEmpty.setOnRefreshListener { - setLoadingMessageEmpty() - fetchAndSetData() - } - } - private fun setupUser(bundle: Bundle) { val accountName = bundle.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) @@ -160,13 +140,12 @@ class NotificationsActivity : } private fun showError() { - runOnUiThread { + requireActivity().runOnUiThread { setEmptyContent( getString(R.string.notifications_no_results_headline), getString(R.string.account_not_found) ) } - return } @Suppress("NestedBlockDepth") @@ -182,19 +161,21 @@ class NotificationsActivity : } else { val pushUrl = resources.getString(R.string.push_server_url) - if (pushUrl.isEmpty() && isFlavourGPlay()) { + if (pushUrl.isEmpty() && BuildHelper.isFlavourGPlay()) { // branded client without push server return } if (pushUrl.isEmpty()) { - snackbar = Snackbar.make( - binding.emptyList.emptyListView, - R.string.push_notifications_not_implemented, - Snackbar.LENGTH_INDEFINITE - ) + snackbar = binding?.emptyList?.emptyListView?.let { + Snackbar.make( + it, + R.string.push_notifications_not_implemented, + Snackbar.LENGTH_INDEFINITE + ) + } } else { - val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(this) + val arbitraryDataProvider = ArbitraryDataProviderImpl(requireActivity()) val accountName: String = if (optionalUser?.isPresent == true) { optionalUser?.get()?.accountName ?: "" } else { @@ -206,19 +187,23 @@ class NotificationsActivity : ) if (usesOldLogin) { - snackbar = Snackbar.make( - binding.emptyList.emptyListView, - R.string.push_notifications_old_login, - Snackbar.LENGTH_INDEFINITE - ) + snackbar = binding?.emptyList?.emptyListView?.let { + Snackbar.make( + it, + R.string.push_notifications_old_login, + Snackbar.LENGTH_INDEFINITE + ) + } } else { val pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH) if (pushValue.isEmpty()) { - snackbar = Snackbar.make( - binding.emptyList.emptyListView, - R.string.push_notifications_temp_error, - Snackbar.LENGTH_INDEFINITE - ) + snackbar = binding?.emptyList?.emptyListView?.let { + Snackbar.make( + it, + R.string.push_notifications_temp_error, + Snackbar.LENGTH_INDEFINITE + ) + } } } } @@ -229,56 +214,56 @@ class NotificationsActivity : } } - /** - * sets up the UI elements and loads all notification items. - */ private fun setupContent() { - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) - setLoadingMessageEmpty() - val layoutManager = LinearLayoutManager(this) - binding.list.layoutManager = layoutManager - fetchAndSetData() + binding?.run { + emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) + setLoadingMessageEmpty() + val layoutManager = LinearLayoutManager(requireContext()) + list.layoutManager = layoutManager + fetchAndSetData() + } + } @VisibleForTesting - fun populateList(notifications: List?) { + fun populateList(notifications: List) { initializeAdapter() adapter?.setNotificationItems(notifications) - binding.loadingContent.visibility = View.GONE + binding?.run { + loadingContent.visibility = View.GONE - if (notifications?.isNotEmpty() == true) { - binding.swipeContainingEmpty.visibility = View.GONE - binding.swipeContainingList.visibility = View.VISIBLE - } else { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ) - binding.swipeContainingList.visibility = View.GONE - binding.swipeContainingEmpty.visibility = View.VISIBLE + if (notifications.isNotEmpty()) { + swipeContainingEmpty.visibility = View.GONE + swipeContainingList.visibility = View.VISIBLE + } else { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.notifications_no_results_message) + ) + swipeContainingList.visibility = View.GONE + swipeContainingEmpty.visibility = View.VISIBLE + } } } private fun fetchAndSetData() { - val t = Thread { + lifecycleScope.launch(Dispatchers.IO) { initializeAdapter() val getRemoteNotificationOperation = GetNotificationsRemoteOperation() val result = client?.let { getRemoteNotificationOperation.execute(it) } - if (result?.isSuccess == true && result.resultData != null) { - runOnUiThread { populateList(result.resultData) } - } else { - Log_OC.d(TAG, result?.logMessage) - // show error - runOnUiThread { + withContext(Dispatchers.Main) { + if (result?.isSuccess == true && result.resultData != null) { + populateList(result.resultData ?: listOf()) + } else { + Log_OC.d(TAG, result?.logMessage) setEmptyContent( getString(R.string.notifications_no_results_headline), - result?.getLogMessage(this) + result?.getLogMessage(requireContext()) ) } + hideRefreshLayoutLoader() } - hideRefreshLayoutLoader() } - t.start() } private fun initializeClient() { @@ -286,7 +271,7 @@ class NotificationsActivity : try { val user = optionalUser?.get() client = clientFactory.createNextcloudClient(user) - } catch (e: CreationException) { + } catch (e: ClientFactory.CreationException) { Log_OC.e(TAG, "Error initializing client", e) } } @@ -296,82 +281,87 @@ class NotificationsActivity : initializeClient() if (adapter == null) { adapter = NotificationListAdapter(client, this, viewThemeUtils) - binding.list.adapter = adapter + binding?.list?.adapter = adapter } } private fun hideRefreshLayoutLoader() { - runOnUiThread { - binding.swipeContainingList.isRefreshing = false - binding.swipeContainingEmpty.isRefreshing = false - } + binding?.swipeContainingList?.isRefreshing = false + binding?.swipeContainingEmpty?.isRefreshing = false } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_notifications, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - var retval = true - val itemId = item.itemId - when (itemId) { - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - } - - R.id.action_empty_notifications -> { - DeleteAllNotificationsTask(client, this).execute() + private fun setupMenu() { + val menuHost: MenuHost = requireActivity() + menuHost.addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_notifications, menu) } - else -> { - retval = super.onOptionsItemSelected(item) + override fun onMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_empty_notifications -> { + lifecycleScope.launch(Dispatchers.IO) { + val result = DeleteAllNotificationsRemoteOperation().execute(client!!) + withContext(Dispatchers.Main) { + onRemovedAllNotifications(result.isSuccess) + } + } + + true + } + else -> false + } } - } - return retval + }, viewLifecycleOwner, Lifecycle.State.RESUMED) } private fun setLoadingMessage() { - binding.swipeContainingEmpty.visibility = View.GONE + binding?.swipeContainingEmpty?.visibility = View.GONE } @VisibleForTesting fun setLoadingMessageEmpty() { - binding.swipeContainingList.visibility = View.GONE - binding.emptyList.emptyListView.visibility = View.GONE - binding.loadingContent.visibility = View.VISIBLE + binding?.run { + swipeContainingList.visibility = View.GONE + emptyList.emptyListView.visibility = View.GONE + loadingContent.visibility = View.VISIBLE + } } @VisibleForTesting fun setEmptyContent(headline: String?, message: String?) { - binding.swipeContainingList.visibility = View.GONE - binding.loadingContent.visibility = View.GONE - binding.swipeContainingEmpty.visibility = View.VISIBLE - binding.emptyList.emptyListView.visibility = View.VISIBLE - binding.emptyList.emptyListViewHeadline.text = headline - binding.emptyList.emptyListViewText.text = message - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) - binding.emptyList.emptyListViewText.visibility = View.VISIBLE - binding.emptyList.emptyListIcon.visibility = View.VISIBLE + binding?.run { + swipeContainingList.visibility = View.GONE + loadingContent.visibility = View.GONE + swipeContainingEmpty.visibility = View.VISIBLE + emptyList.emptyListView.visibility = View.VISIBLE + emptyList.emptyListViewHeadline.text = headline + emptyList.emptyListViewText.text = message + emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) + emptyList.emptyListViewText.visibility = View.VISIBLE + emptyList.emptyListIcon.visibility = View.VISIBLE + } } override fun onRemovedNotification(isSuccess: Boolean) { if (!isSuccess) { - DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed)) + DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.remove_notification_failed)) fetchAndSetData() } } - override fun removeNotification(holder: NotificationViewHolder) { + override fun removeNotification(holder: NotificationListAdapter.NotificationViewHolder) { adapter?.removeNotification(holder) if (adapter?.itemCount == 0) { setEmptyContent( getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message) ) - binding.swipeContainingList.visibility = View.GONE - binding.loadingContent.visibility = View.GONE - binding.swipeContainingEmpty.visibility = View.VISIBLE + binding?.run { + swipeContainingList.visibility = View.GONE + loadingContent.visibility = View.GONE + swipeContainingEmpty.visibility = View.VISIBLE + } } } @@ -382,24 +372,26 @@ class NotificationsActivity : getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message) ) - binding.loadingContent.visibility = View.GONE - binding.swipeContainingList.visibility = View.GONE - binding.swipeContainingEmpty.visibility = View.VISIBLE + binding?.run { + loadingContent.visibility = View.GONE + swipeContainingList.visibility = View.GONE + swipeContainingEmpty.visibility = View.VISIBLE + } } else { - DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed)) + DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.clear_notifications_failed)) } } - override fun onActionCallback(isSuccess: Boolean, notification: Notification, holder: NotificationViewHolder) { + override fun onActionCallback(isSuccess: Boolean, notification: Notification, holder: NotificationListAdapter.NotificationViewHolder) { if (isSuccess) { adapter?.removeNotification(holder) } else { adapter?.setButtons(holder, notification) - DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed)) + DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.notification_action_failed)) } } companion object { - private val TAG = NotificationsActivity::class.java.simpleName + private val TAG = NotificationsFragment::class.java.simpleName } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt index 0612469e2dbc..4fafe5821af0 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt @@ -12,25 +12,27 @@ import android.content.Intent import android.os.Bundle import android.view.MenuItem import androidx.activity.OnBackPressedCallback +import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentContainerView import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityNavigatorBinding import com.owncloud.android.ui.activity.DrawerActivity import dagger.android.support.AndroidSupportInjection class NavigatorActivity : DrawerActivity() { + private lateinit var binding: ActivityNavigatorBinding private lateinit var navigator: Navigator // region Lifecycle Methods override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityNavigatorBinding.inflate(layoutInflater) setContentView(R.layout.activity_navigator) val screen = intent.getParcelableArgument(EXTRA_SCREEN, NavigatorScreen::class.java) ?: return - val fragmentContainerView = findViewById(R.id.fragment_container_view) - navigator = Navigator(supportFragmentManager, fragmentContainerView) + navigator = Navigator(supportFragmentManager, binding.fragmentContainerView) setupBackPressedHandler() pushOrRestoreScreen(savedInstanceState, screen) } @@ -45,10 +47,16 @@ class NavigatorActivity : DrawerActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - if (isDrawerOpen) { - closeDrawer() + val currentScreen = navigator.getTopScreen() + + if (currentScreen?.hasDrawer == false) { + onBackPressedDispatcher.onBackPressed() } else { - openDrawer() + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } } return true } @@ -97,7 +105,15 @@ class NavigatorActivity : DrawerActivity() { setupHomeSearchToolbarWithSortAndListButtons() } updateActionBarTitleAndHomeButtonByString(getString(titleId)) - setupDrawer(menuItemId) + + if (screen.hasDrawer) { + setupDrawer(menuItemId) + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + } else { + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_foreground) + } } private fun setupBackPressedHandler() { diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt index 90a0601d21b7..c2d504b1eb9a 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt @@ -12,9 +12,10 @@ import androidx.fragment.app.Fragment import com.owncloud.android.R import com.owncloud.android.ui.fragment.ActivitiesFragment import com.owncloud.android.ui.fragment.community.CommunityFragment +import com.owncloud.android.ui.fragment.notifications.NotificationsFragment import kotlinx.parcelize.Parcelize -sealed class NavigatorScreen(val tag: String) : Parcelable { +sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : Parcelable { @Parcelize object Activities : NavigatorScreen(ACTIVITIES_TAG) @@ -22,13 +23,18 @@ sealed class NavigatorScreen(val tag: String) : Parcelable { @Parcelize object Community : NavigatorScreen(COMMUNITY_TAG) + @Parcelize + object Notifications : NavigatorScreen(NOTIFICATIONS_TAG, hasDrawer = false) + companion object { private const val ACTIVITIES_TAG = "Activities" private const val COMMUNITY_TAG = "Community" + private const val NOTIFICATIONS_TAG = "Notifications" fun fromTag(tag: String?): NavigatorScreen? = when (tag) { ACTIVITIES_TAG -> Activities COMMUNITY_TAG -> Community + NOTIFICATIONS_TAG -> Notifications else -> null } } @@ -36,15 +42,18 @@ sealed class NavigatorScreen(val tag: String) : Parcelable { fun menuItemId(): Int = when (this) { Community -> R.id.nav_community Activities -> R.id.nav_activity + Notifications -> -1 } fun actionBarStyle(): Pair = when (this) { Community -> ActionBarStyle.Plain to R.string.drawer_community Activities -> ActionBarStyle.Plain to R.string.drawer_item_activities + Notifications -> ActionBarStyle.Plain to R.string.drawer_item_notifications } fun toFragment(): Fragment = when (this) { Community -> CommunityFragment() Activities -> ActivitiesFragment() + Notifications -> NotificationsFragment() } } From 3152e355d466fc1e810e9d4342a53979275f1074 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 15:40:06 +0200 Subject: [PATCH 03/18] fix(navigator): simplify logic Signed-off-by: alperozturk96 --- .../ui/adapter/NotificationListAdapter.kt | 343 +++++++++--------- .../notifications/NotificationsFragment.kt | 4 +- 2 files changed, 168 insertions(+), 179 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 7163707150fe..21cf966ebfc7 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -2,6 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2023 TSI-mc + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2018-2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Andy Scherzinger @@ -12,7 +13,6 @@ package com.owncloud.android.ui.adapter import android.annotation.SuppressLint import android.content.Intent -import android.content.res.Resources import android.graphics.Typeface import android.text.Spannable import android.text.SpannableStringBuilder @@ -35,6 +35,7 @@ import com.google.android.material.button.MaterialButton import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.GlideHelper +import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.NotificationListItemBinding import com.owncloud.android.lib.common.utils.Log_OC @@ -50,9 +51,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -/** - * This Adapter populates a RecyclerView with all notifications for an account within the app. - */ class NotificationListAdapter( private val client: NextcloudClient?, private val fragment: NotificationsFragment, @@ -78,6 +76,10 @@ class NotificationListAdapter( ) } + override fun getItemCount(): Int { + return notificationsList.size + } + override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { val notification = notificationsList[position] holder.binding.datetime.text = DisplayUtils.getRelativeTimestamp( @@ -85,65 +87,77 @@ class NotificationListAdapter( notification.getDatetime().time ) + bindSubject(holder, notification) + bindMessage(holder, notification) + bindIcon(holder, notification) + colorViewHolder(holder) + bindButtons(holder, notification) + } + + private fun bindSubject(holder: NotificationViewHolder, notification: Notification) { val file = notification.subjectRichParameters[FILE] - var subject = notification.getSubject() if (file == null && !TextUtils.isEmpty(notification.getLink())) { - subject = "$subject ↗" - holder.binding.subject.setTypeface( - holder.binding.subject.typeface, - Typeface.BOLD - ) - holder.binding.subject.setOnClickListener { - DisplayUtils.startLinkIntent( - fragment.requireActivity(), - notification.getLink() - ) + val subject = "${notification.getSubject()} ↗" + holder.binding.subject.run { + setTypeface(typeface, Typeface.BOLD) + text = subject + setOnClickListener { + DisplayUtils.startLinkIntent(fragment.requireActivity(), notification.getLink()) + } } - holder.binding.subject.text = subject } else { - if (!TextUtils.isEmpty(notification.subjectRich)) { - holder.binding.subject.text = makeSpecialPartsBold(notification) - } else { - holder.binding.subject.text = subject - } - - if (file != null && !TextUtils.isEmpty(file.id)) { - holder.binding.subject.setOnClickListener { - val intent = Intent(fragment.requireActivity(), FileDisplayActivity::class.java) - intent.setAction(Intent.ACTION_VIEW) - intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id) - fragment.requireActivity().startActivity(intent) + holder.binding.subject.run { + text = if (!TextUtils.isEmpty(notification.subjectRich)) { + makeSpecialPartsBold(notification) + } else { + notification.getSubject() + } + if (file?.id?.isNotEmpty() == true) { + setOnClickListener { + val intent = Intent(fragment.requireActivity(), FileDisplayActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(FileDisplayActivity.KEY_FILE_ID, file.id) + } + fragment.requireActivity().startActivity(intent) + } } } } + } - if (notification.getMessage() != null && !notification.getMessage().isEmpty()) { - holder.binding.message.text = notification.getMessage() - holder.binding.message.visibility = View.VISIBLE - } else { - holder.binding.message.visibility = View.GONE + private fun bindMessage(holder: NotificationViewHolder, notification: Notification) { + holder.binding.message.run { + if (notification.getMessage() != null && !notification.getMessage().isEmpty()) { + text = notification.getMessage() + visibility = View.VISIBLE + } else { + visibility = View.GONE + } } + } - if (!TextUtils.isEmpty(notification.getIcon())) { - fragment.lifecycleScope.launch(Dispatchers.IO) { - try { - withContext(Dispatchers.Main) { - GlideHelper - .loadIntoImageView( - fragment.requireContext(), - client, - notification.getIcon(), - holder.binding.icon, - R.drawable.ic_notification, - false - ) - } - } catch (e: Exception) { - Log_OC.e("RichDocumentsTemplateAdapter", "Exception setData: " + e) + private fun bindIcon(holder: NotificationViewHolder, notification: Notification) { + if (notification.getIcon().isNullOrEmpty()) return + + fragment.lifecycleScope.launch(Dispatchers.IO) { + runCatching { + withContext(Dispatchers.Main) { + GlideHelper.loadIntoImageView( + fragment.requireContext(), + client, + notification.getIcon(), + holder.binding.icon, + R.drawable.ic_notification, + false + ) } + }.onFailure { e -> + Log_OC.e("RichDocumentsTemplateAdapter", "exception setData: $e") } } + } + private fun colorViewHolder(holder: NotificationViewHolder) { viewThemeUtils.platform.run { colorImageView(holder.binding.icon, ColorRole.ON_SURFACE_VARIANT) colorImageView(holder.binding.dismiss, ColorRole.ON_SURFACE_VARIANT) @@ -151,10 +165,105 @@ class NotificationListAdapter( colorTextView(holder.binding.message, ColorRole.ON_SURFACE_VARIANT) colorTextView(holder.binding.datetime, ColorRole.ON_SURFACE_VARIANT) } + } + private fun getPrimaryButton( + holder: NotificationViewHolder, + action: Action, + notification: Notification, + params: LinearLayout.LayoutParams + ): MaterialButton { + return MaterialButton(fragment.requireContext()).apply { + setAllCaps(false) + text = action.label + setCornerRadiusResource(R.dimen.button_corner_radius) + setLayoutParams(params) + setGravity(Gravity.CENTER) + setOnClickListener { + onPrimaryAction(holder, action, notification) + } + } + } + + private fun onPrimaryAction(holder: NotificationViewHolder, action: Action, notification: Notification) { + setButtonEnabled(holder, false) + if (ACTION_TYPE_WEB == action.type) { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(action.link?.toUri()) + fragment.requireActivity().startActivity(intent) + } else { + NotificationExecuteActionTask( + client!!, + holder, + notification, + fragment + ).execute(action) + } + } + + private fun getMoreButton( + overflowActions: ArrayList, + params: LinearLayout.LayoutParams, + holder: NotificationViewHolder, + notification: Notification + ): MaterialButton { + return MaterialButton(fragment.requireContext()).apply { + setBackgroundColor( + ResourcesCompat.getColor( + resources, + android.R.color.transparent, + null + ) + ) + setAllCaps(false) + setText(R.string.more) + setCornerRadiusResource(R.dimen.button_corner_radius) + setLayoutParams(params) + setGravity(Gravity.CENTER) + setOnClickListener { + val popup = PopupMenu(fragment.requireContext(), this) + for (action in overflowActions) { + popup.menu.add(action.label) + .setOnMenuItemClickListener { + onPrimaryAction(holder, action, notification) + true + } + } + popup.show() + } + } + } - setButtons(holder, notification) + private fun getButton( + action: Action, + holder: NotificationViewHolder, + params: LinearLayout.LayoutParams, + notification: Notification + ): MaterialButton { + return MaterialButton(fragment.requireContext()).apply { + if (action.primary) { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(this) + } else { + setBackgroundColor( + ResourcesCompat.getColor( + resources, + android.R.color.transparent, + null + ) + ) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(this) + } + setAllCaps(false) + text = action.label + setCornerRadiusResource(R.dimen.button_corner_radius) + setLayoutParams(params) + setOnClickListener { + onPrimaryAction(holder, action, notification) + } + } + } + fun bindButtons(holder: NotificationViewHolder, notification: Notification) { holder.binding.dismiss.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { val result = DeleteNotificationRemoteOperation(notification.notificationId) @@ -164,65 +273,22 @@ class NotificationListAdapter( } } } - } - - override fun getItemCount(): Int { - return notificationsList.size - } - fun setButtons(holder: NotificationViewHolder, notification: Notification) { - // add action buttons holder.binding.buttons.removeAllViews() - val resources: Resources = fragment.resources - val params: LinearLayout.LayoutParams = LinearLayout.LayoutParams( + val resources = fragment.resources + val params = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ) - params.setMargins( - resources.getDimensionPixelOffset(R.dimen.standard_quarter_margin), - 0, - resources.getDimensionPixelOffset(R.dimen.standard_half_margin), - 0 - ) - + params.setMargins(resources.getDimensionPixelOffset(R.dimen.standard_quarter_margin), 0, resources.getDimensionPixelOffset(R.dimen.standard_half_margin), 0) val overflowActions = ArrayList() - - if (notification.getActions().isNotEmpty()) { - holder.binding.buttons.visibility = View.VISIBLE - } else { - holder.binding.buttons.visibility = View.GONE - } + holder.binding.buttons.setVisibleIf(notification.getActions().isNotEmpty()) if (notification.getActions().size > 2) { for (action in notification.getActions()) { if (action.primary) { - val button: MaterialButton = MaterialButton(fragment.requireContext()) - button.setAllCaps(false) - - button.text = action.label - button.setCornerRadiusResource(R.dimen.button_corner_radius) - - button.setLayoutParams(params) - button.setGravity(Gravity.CENTER) - - button.setOnClickListener { - setButtonEnabled(holder, false) - if (ACTION_TYPE_WEB == action.type) { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(action.link?.toUri()) - fragment.requireActivity().startActivity(intent) - } else { - NotificationExecuteActionTask( - client!!, - holder, - notification, - fragment - ) - .execute(action) - } - } - + val button = getPrimaryButton(holder, action, notification, params) viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) holder.binding.buttons.addView(button) } else { @@ -230,89 +296,12 @@ class NotificationListAdapter( } } - // further actions - val moreButton = MaterialButton(fragment.requireContext()) - moreButton.setBackgroundColor( - ResourcesCompat.getColor( - resources, - android.R.color.transparent, - null - ) - ) + val moreButton = getMoreButton(overflowActions, params, holder, notification) viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(moreButton) - - moreButton.setAllCaps(false) - moreButton.setText(R.string.more) - moreButton.setCornerRadiusResource(R.dimen.button_corner_radius) - moreButton.setLayoutParams(params) - moreButton.setGravity(Gravity.CENTER) - moreButton.setOnClickListener { - val popup = PopupMenu(fragment.requireContext(), moreButton) - for (action in overflowActions) { - popup.menu.add(action.label) - .setOnMenuItemClickListener { - setButtonEnabled(holder, false) - if (ACTION_TYPE_WEB == action.type) { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(action.link?.toUri()) - fragment.requireActivity().startActivity(intent) - } else { - NotificationExecuteActionTask( - client!!, - holder, - notification, - fragment - ) - .execute(action) - } - true - } - } - popup.show() - } - holder.binding.buttons.addView(moreButton) } else { for (action in notification.getActions()) { - val button = MaterialButton(fragment.requireContext()) - - if (action.primary) { - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) - } else { - button.setBackgroundColor( - ResourcesCompat.getColor( - resources, - android.R.color.transparent, - null - ) - ) - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(button) - } - - button.setAllCaps(false) - - button.text = action.label - button.setCornerRadiusResource(R.dimen.button_corner_radius) - - button.setLayoutParams(params) - - button.setOnClickListener { - setButtonEnabled(holder, false) - if (ACTION_TYPE_WEB == action.type) { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(action.link?.toUri()) - fragment.requireActivity().startActivity(intent) - } else { - NotificationExecuteActionTask( - client!!, - holder, - notification, - fragment - ) - .execute(action) - } - } - + val button = getButton(action, holder, params, notification) holder.binding.buttons.addView(button) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 90332fec0dfa..5cabc446890f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -386,7 +386,7 @@ class NotificationsFragment : if (isSuccess) { adapter?.removeNotification(holder) } else { - adapter?.setButtons(holder, notification) + adapter?.bindButtons(holder, notification) DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.notification_action_failed)) } } @@ -394,4 +394,4 @@ class NotificationsFragment : companion object { private val TAG = NotificationsFragment::class.java.simpleName } -} \ No newline at end of file +} From a1d185040bb7b0552effe8bca2f34606a9443bfa Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 15:51:33 +0200 Subject: [PATCH 04/18] Rename .java to .kt Signed-off-by: alperozturk96 --- .../{NotificationsContract.java => NotificationsContract.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/notifications/{NotificationsContract.java => NotificationsContract.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.java b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.java rename to app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt From ab724ab84d1e551f31fa28e832f986d8a12f3e25 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 15:51:34 +0200 Subject: [PATCH 05/18] fix(navigator): simplify logic Signed-off-by: alperozturk96 --- .../ui/adapter/NotificationListAdapter.kt | 329 +++++++++--------- .../NotificationExecuteActionTask.kt | 1 + .../notifications/NotificationsFragment.kt | 316 +++++++---------- .../ui/notifications/NotificationsContract.kt | 19 +- 4 files changed, 301 insertions(+), 364 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 21cf966ebfc7..65421e262301 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -51,42 +51,35 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +@Suppress("TooManyFunctions") class NotificationListAdapter( private val client: NextcloudClient?, private val fragment: NotificationsFragment, private val viewThemeUtils: ViewThemeUtils ) : RecyclerView.Adapter() { + private val styleSpanBold = StyleSpan(Typeface.BOLD) - private val foregroundColorSpanBlack: ForegroundColorSpan = ForegroundColorSpan( + private val foregroundColorSpanBlack = ForegroundColorSpan( ContextCompat.getColor(fragment.requireContext(), R.color.text_color) ) + private val notificationsList = ArrayList() - private val notificationsList: ArrayList = ArrayList() - - @SuppressLint("NotifyDataSetChanged") - fun setNotificationItems(notificationItems: List) { - notificationsList.clear() - notificationsList.addAll(notificationItems) - notifyDataSetChanged() - } + // region Adapter overrides - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder { - return NotificationViewHolder( - NotificationListItemBinding.inflate(LayoutInflater.from(fragment.requireContext())) - ) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NotificationViewHolder( + NotificationListItemBinding.inflate(LayoutInflater.from(fragment.requireContext())) + ) - override fun getItemCount(): Int { - return notificationsList.size - } + override fun getItemCount() = notificationsList.size override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { val notification = notificationsList[position] - holder.binding.datetime.text = DisplayUtils.getRelativeTimestamp( - fragment.requireContext(), - notification.getDatetime().time - ) - + with(holder.binding) { + datetime.text = DisplayUtils.getRelativeTimestamp( + fragment.requireContext(), + notification.getDatetime().time + ) + } bindSubject(holder, notification) bindMessage(holder, notification) bindIcon(holder, notification) @@ -94,11 +87,15 @@ class NotificationListAdapter( bindButtons(holder, notification) } + // endregion + + // region Bind helpers + private fun bindSubject(holder: NotificationViewHolder, notification: Notification) { val file = notification.subjectRichParameters[FILE] if (file == null && !TextUtils.isEmpty(notification.getLink())) { val subject = "${notification.getSubject()} ↗" - holder.binding.subject.run { + holder.binding.subject.apply { setTypeface(typeface, Typeface.BOLD) text = subject setOnClickListener { @@ -106,7 +103,7 @@ class NotificationListAdapter( } } } else { - holder.binding.subject.run { + holder.binding.subject.apply { text = if (!TextUtils.isEmpty(notification.subjectRich)) { makeSpecialPartsBold(notification) } else { @@ -126,9 +123,10 @@ class NotificationListAdapter( } private fun bindMessage(holder: NotificationViewHolder, notification: Notification) { - holder.binding.message.run { - if (notification.getMessage() != null && !notification.getMessage().isEmpty()) { - text = notification.getMessage() + val message = notification.getMessage() + holder.binding.message.apply { + if (!message.isNullOrEmpty()) { + text = message visibility = View.VISIBLE } else { visibility = View.GONE @@ -138,7 +136,6 @@ class NotificationListAdapter( private fun bindIcon(holder: NotificationViewHolder, notification: Notification) { if (notification.getIcon().isNullOrEmpty()) return - fragment.lifecycleScope.launch(Dispatchers.IO) { runCatching { withContext(Dispatchers.Main) { @@ -167,177 +164,142 @@ class NotificationListAdapter( } } - private fun getPrimaryButton( - holder: NotificationViewHolder, - action: Action, - notification: Notification, - params: LinearLayout.LayoutParams - ): MaterialButton { - return MaterialButton(fragment.requireContext()).apply { - setAllCaps(false) - text = action.label - setCornerRadiusResource(R.dimen.button_corner_radius) - setLayoutParams(params) - setGravity(Gravity.CENTER) - setOnClickListener { - onPrimaryAction(holder, action, notification) - } - } - } - - private fun onPrimaryAction(holder: NotificationViewHolder, action: Action, notification: Notification) { - setButtonEnabled(holder, false) - if (ACTION_TYPE_WEB == action.type) { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(action.link?.toUri()) - fragment.requireActivity().startActivity(intent) - } else { - NotificationExecuteActionTask( - client!!, - holder, - notification, - fragment - ).execute(action) - } - } - - private fun getMoreButton( - overflowActions: ArrayList, - params: LinearLayout.LayoutParams, - holder: NotificationViewHolder, - notification: Notification - ): MaterialButton { - return MaterialButton(fragment.requireContext()).apply { - setBackgroundColor( - ResourcesCompat.getColor( - resources, - android.R.color.transparent, - null - ) - ) - setAllCaps(false) - setText(R.string.more) - setCornerRadiusResource(R.dimen.button_corner_radius) - setLayoutParams(params) - setGravity(Gravity.CENTER) - setOnClickListener { - val popup = PopupMenu(fragment.requireContext(), this) - for (action in overflowActions) { - popup.menu.add(action.label) - .setOnMenuItemClickListener { - onPrimaryAction(holder, action, notification) - true - } - } - popup.show() - } - } - } + // endregion - private fun getButton( - action: Action, - holder: NotificationViewHolder, - params: LinearLayout.LayoutParams, - notification: Notification - ): MaterialButton { - return MaterialButton(fragment.requireContext()).apply { - if (action.primary) { - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(this) - } else { - setBackgroundColor( - ResourcesCompat.getColor( - resources, - android.R.color.transparent, - null - ) - ) - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(this) - } - setAllCaps(false) - text = action.label - setCornerRadiusResource(R.dimen.button_corner_radius) - setLayoutParams(params) - setOnClickListener { - onPrimaryAction(holder, action, notification) - } - } - } + // region Button binding fun bindButtons(holder: NotificationViewHolder, notification: Notification) { holder.binding.dismiss.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { - val result = DeleteNotificationRemoteOperation(notification.notificationId) - .execute(client!!) - withContext(Dispatchers.Main) { - fragment.onRemovedNotification(result.isSuccess) - } + val result = DeleteNotificationRemoteOperation(notification.notificationId).execute(client!!) + withContext(Dispatchers.Main) { fragment.onRemovedNotification(result.isSuccess) } } } - holder.binding.buttons.removeAllViews() + val actions = notification.getActions() + holder.binding.buttons.apply { + removeAllViews() + setVisibleIf(actions.isNotEmpty()) + if (actions.isEmpty()) return + } - val resources = fragment.resources - val params = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - params.setMargins(resources.getDimensionPixelOffset(R.dimen.standard_quarter_margin), 0, resources.getDimensionPixelOffset(R.dimen.standard_half_margin), 0) - val overflowActions = ArrayList() - holder.binding.buttons.setVisibleIf(notification.getActions().isNotEmpty()) + val params = buttonLayoutParams() - if (notification.getActions().size > 2) { - for (action in notification.getActions()) { + if (actions.size > 2) { + val overflowActions = ArrayList() + for (action in actions) { if (action.primary) { - val button = getPrimaryButton(holder, action, notification, params) - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) - holder.binding.buttons.addView(button) + addPrimaryButton(holder, action, notification, params) } else { overflowActions.add(action) } } - - val moreButton = getMoreButton(overflowActions, params, holder, notification) + val moreButton = + buildButton(transparent = true, label = fragment.getString(R.string.more), params = params) { + showOverflowMenu(it, overflowActions, holder, notification) + } viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(moreButton) holder.binding.buttons.addView(moreButton) } else { - for (action in notification.getActions()) { - val button = getButton(action, holder, params, notification) + for (action in actions) { + val button = buildButton(transparent = !action.primary, label = action.label, params = params) { + onActionClicked(holder, action, notification) + } + if (action.primary) { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) + } else { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(button) + } holder.binding.buttons.addView(button) } } } - private fun makeSpecialPartsBold(notification: Notification): SpannableStringBuilder { - var text = notification.getSubjectRich() - val ssb = SpannableStringBuilder(text) - - var openingBrace = text.indexOf('{') - var closingBrace: Int - var replaceablePart: String? - while (openingBrace != -1) { - closingBrace = text.indexOf('}', openingBrace) + 1 - replaceablePart = text.substring(openingBrace + 1, closingBrace - 1) + private fun addPrimaryButton( + holder: NotificationViewHolder, + action: Action, + notification: Notification, + params: LinearLayout.LayoutParams + ) { + val button = buildButton(transparent = false, label = action.label, params = params) { + onActionClicked(holder, action, notification) + } + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(button) + holder.binding.buttons.addView(button) + } - val richObject = notification.subjectRichParameters.get(replaceablePart) - if (richObject != null) { - val name = richObject.name - ssb.replace(openingBrace, closingBrace, name) - text = ssb.toString() - closingBrace = openingBrace + name!!.length + private fun buildButton( + transparent: Boolean, + label: String?, + params: LinearLayout.LayoutParams, + onClick: (View) -> Unit + ): MaterialButton = MaterialButton(fragment.requireContext()).apply { + if (transparent) { + setBackgroundColor(ResourcesCompat.getColor(resources, android.R.color.transparent, null)) + } + setAllCaps(false) + text = label + setCornerRadiusResource(R.dimen.button_corner_radius) + layoutParams = params + setGravity(Gravity.CENTER) + setOnClickListener(onClick) + } - ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0) - ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + private fun showOverflowMenu( + anchor: View, + overflowActions: List, + holder: NotificationViewHolder, + notification: Notification + ) { + PopupMenu(fragment.requireContext(), anchor).apply { + for (action in overflowActions) { + menu.add(action.label).setOnMenuItemClickListener { + onActionClicked(holder, action, notification) + true + } } - openingBrace = text.indexOf('{', closingBrace) + show() } + } - return ssb + private fun onActionClicked(holder: NotificationViewHolder, action: Action, notification: Notification) { + setButtonEnabled(holder, false) + if (ACTION_TYPE_WEB == action.type) { + fragment.requireActivity().startActivity( + Intent(Intent.ACTION_VIEW).apply { data = action.link?.toUri() } + ) + } else { + NotificationExecuteActionTask(client!!, holder, notification, fragment).execute(action) + } + } + + private fun buttonLayoutParams() = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + val resources = fragment.resources + setMargins( + resources.getDimensionPixelOffset(R.dimen.standard_quarter_margin), + 0, + resources.getDimensionPixelOffset(R.dimen.standard_half_margin), + 0 + ) + } + + // endregion + + // region Data manipulation + + @SuppressLint("NotifyDataSetChanged") + fun setNotificationItems(notificationItems: List) { + notificationsList.clear() + notificationsList.addAll(notificationItems) + notifyDataSetChanged() } fun removeNotification(holder: NotificationViewHolder) { val position = holder.bindingAdapterPosition - - if (position >= 0 && position < notificationsList.size) { + if (position in 0 until notificationsList.size) { notificationsList.removeAt(position) notifyItemRemoved(position) notifyItemRangeChanged(position, notificationsList.size) @@ -351,15 +313,38 @@ class NotificationListAdapter( } fun setButtonEnabled(holder: NotificationViewHolder, enabled: Boolean) { - for (i in 0.. * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ - package com.owncloud.android.ui.fragment.notifications import android.os.Bundle @@ -34,6 +33,7 @@ import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.BuildHelper +import com.nextcloud.utils.extensions.getTypedActivity import com.owncloud.android.R import com.owncloud.android.databinding.NotificationsLayoutBinding import com.owncloud.android.datamodel.ArbitraryDataProviderImpl @@ -41,6 +41,7 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.notifications.DeleteAllNotificationsRemoteOperation import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.adapter.NotificationListAdapter import com.owncloud.android.ui.notifications.NotificationsContract import com.owncloud.android.utils.DisplayUtils @@ -52,67 +53,59 @@ import kotlinx.coroutines.withContext import java.util.Optional import javax.inject.Inject -/** - * Activity displaying all server side stored notification items. - */ -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "ReturnCount") class NotificationsFragment : Fragment(), NotificationsContract.View, Injectable { private var binding: NotificationsLayoutBinding? = null - private var adapter: NotificationListAdapter? = null private var snackbar: Snackbar? = null private var client: NextcloudClient? = null private var optionalUser: Optional? = null - @Inject - lateinit var viewThemeUtils: ViewThemeUtils + @Inject lateinit var viewThemeUtils: ViewThemeUtils - @Inject - lateinit var accountManager: UserAccountManager + @Inject lateinit var accountManager: UserAccountManager - @Inject - lateinit var clientFactory: ClientFactory + @Inject lateinit var clientFactory: ClientFactory - @Inject - lateinit var preferences: AppPreferences + @Inject lateinit var preferences: AppPreferences - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + // region Lifecycle + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = NotificationsLayoutBinding.inflate(inflater, container, false) - val binding = binding!! - return binding.root + return binding!!.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Log_OC.v(TAG, "onViewCreated() start") - setupMenu() initUser() - setupContainingList() + setupSwipeRefresh() setupPushWarning() setupContent() + if (optionalUser?.isPresent == false) showError() + } - if (optionalUser?.isPresent == false) { - showError() - } + override fun onDestroyView() { + super.onDestroyView() + binding = null } + // endregion + // region Setup private fun initUser() { optionalUser = Optional.of(accountManager.user) - arguments?.let { bundle -> - setupUser(bundle) + arguments?.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT)?.let { accountName -> + if (optionalUser?.get()?.accountName.equals(accountName, ignoreCase = true)) { + accountManager.setCurrentOwnCloudAccount(accountName) + } } } - private fun setupContainingList() { + private fun setupSwipeRefresh() { binding?.run { viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingList) viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingEmpty) @@ -128,160 +121,105 @@ class NotificationsFragment : } } - private fun setupUser(bundle: Bundle) { - val accountName = bundle.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) - - if (accountName != null && optionalUser?.isPresent == true) { - val user = optionalUser?.get() - if (user?.accountName.equals(accountName, ignoreCase = true)) { - accountManager.setCurrentOwnCloudAccount(accountName) - } + private fun setupContent() { + binding?.run { + emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) + setLoadingMessageEmpty() + list.layoutManager = LinearLayoutManager(requireContext()) + fetchAndSetData() } } - private fun showError() { - requireActivity().runOnUiThread { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.account_not_found) - ) - } + private fun setupMenu() { + (requireActivity() as MenuHost).addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_notifications, menu) + } + + override fun onMenuItemSelected(item: MenuItem): Boolean { + if (item.itemId != R.id.action_empty_notifications) return false + lifecycleScope.launch(Dispatchers.IO) { + val result = DeleteAllNotificationsRemoteOperation().execute(client!!) + withContext(Dispatchers.Main) { onRemovedAllNotifications(result.isSuccess) } + } + return true + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) } - @Suppress("NestedBlockDepth") private fun setupPushWarning() { - if (!resources.getBoolean(R.bool.show_push_warning)) { + if (!resources.getBoolean(R.bool.show_push_warning)) return + + if (snackbar?.isShown == false) { + snackbar?.show() return } - if (snackbar != null) { - if (snackbar?.isShown == false) { - snackbar?.show() - } - } else { - val pushUrl = resources.getString(R.string.push_server_url) - - if (pushUrl.isEmpty() && BuildHelper.isFlavourGPlay()) { - // branded client without push server - return - } + val pushUrl = resources.getString(R.string.push_server_url) + if (pushUrl.isEmpty() && BuildHelper.isFlavourGPlay()) return - if (pushUrl.isEmpty()) { - snackbar = binding?.emptyList?.emptyListView?.let { - Snackbar.make( - it, - R.string.push_notifications_not_implemented, - Snackbar.LENGTH_INDEFINITE - ) - } - } else { - val arbitraryDataProvider = ArbitraryDataProviderImpl(requireActivity()) - val accountName: String = if (optionalUser?.isPresent == true) { - optionalUser?.get()?.accountName ?: "" - } else { - "" - } - val usesOldLogin = arbitraryDataProvider.getBooleanValue( - accountName, - UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD - ) - - if (usesOldLogin) { - snackbar = binding?.emptyList?.emptyListView?.let { - Snackbar.make( - it, - R.string.push_notifications_old_login, - Snackbar.LENGTH_INDEFINITE - ) - } - } else { - val pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH) - if (pushValue.isEmpty()) { - snackbar = binding?.emptyList?.emptyListView?.let { - Snackbar.make( - it, - R.string.push_notifications_temp_error, - Snackbar.LENGTH_INDEFINITE - ) - } - } - } - } - - if (snackbar != null && snackbar?.isShown == false) { - snackbar?.show() - } + val messageRes = when { + pushUrl.isEmpty() -> R.string.push_notifications_not_implemented + isUsingOldLogin() -> R.string.push_notifications_old_login + isPushValueEmpty() -> R.string.push_notifications_temp_error + else -> return } - } - private fun setupContent() { - binding?.run { - emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) - setLoadingMessageEmpty() - val layoutManager = LinearLayoutManager(requireContext()) - list.layoutManager = layoutManager - fetchAndSetData() + snackbar = binding?.emptyList?.emptyListView?.let { + Snackbar.make(it, messageRes, Snackbar.LENGTH_INDEFINITE).also { s -> s.show() } } - } - @VisibleForTesting - fun populateList(notifications: List) { - initializeAdapter() - adapter?.setNotificationItems(notifications) - binding?.run { - loadingContent.visibility = View.GONE + private fun isUsingOldLogin(): Boolean { + val accountName = optionalUser?.orElse(null)?.accountName ?: return false + return ArbitraryDataProviderImpl(requireActivity()) + .getBooleanValue(accountName, UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD) + } - if (notifications.isNotEmpty()) { - swipeContainingEmpty.visibility = View.GONE - swipeContainingList.visibility = View.VISIBLE - } else { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ) - swipeContainingList.visibility = View.GONE - swipeContainingEmpty.visibility = View.VISIBLE - } - } + private fun isPushValueEmpty(): Boolean { + val accountName = optionalUser?.orElse(null)?.accountName ?: return true + return ArbitraryDataProviderImpl(requireActivity()).getValue(accountName, PushUtils.KEY_PUSH).isEmpty() } + // endregion + // region Data loading private fun fetchAndSetData() { lifecycleScope.launch(Dispatchers.IO) { initializeAdapter() - val getRemoteNotificationOperation = GetNotificationsRemoteOperation() - val result = client?.let { getRemoteNotificationOperation.execute(it) } + val result = client?.let { GetNotificationsRemoteOperation().execute(it) } withContext(Dispatchers.Main) { if (result?.isSuccess == true && result.resultData != null) { populateList(result.resultData ?: listOf()) } else { - Log_OC.d(TAG, result?.logMessage) - setEmptyContent( - getString(R.string.notifications_no_results_headline), - result?.getLogMessage(requireContext()) - ) + try { + Log_OC.d(TAG, result?.logMessage) + setEmptyContent( + getString(R.string.notifications_no_results_headline), + result?.getLogMessage(requireContext()) + ) + } catch (_: Exception) { + } } hideRefreshLayoutLoader() } } } - private fun initializeClient() { - if (client == null && optionalUser?.isPresent == true) { - try { - val user = optionalUser?.get() - client = clientFactory.createNextcloudClient(user) - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Error initializing client", e) - } - } - } - private fun initializeAdapter() { - initializeClient() - if (adapter == null) { - adapter = NotificationListAdapter(client, this, viewThemeUtils) - binding?.list?.adapter = adapter + lifecycleScope.launch { + val baseActivity = getTypedActivity(BaseActivity::class.java) + client = baseActivity?.clientRepository?.getNextcloudClient() + + withContext(Dispatchers.Main) { + if (adapter == null) { + adapter = NotificationListAdapter(client, this@NotificationsFragment, viewThemeUtils) + binding?.list?.adapter = adapter + } + } } } @@ -289,30 +227,27 @@ class NotificationsFragment : binding?.swipeContainingList?.isRefreshing = false binding?.swipeContainingEmpty?.isRefreshing = false } + // endregion - private fun setupMenu() { - val menuHost: MenuHost = requireActivity() - menuHost.addMenuProvider(object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.activity_notifications, menu) - } - - override fun onMenuItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_empty_notifications -> { - lifecycleScope.launch(Dispatchers.IO) { - val result = DeleteAllNotificationsRemoteOperation().execute(client!!) - withContext(Dispatchers.Main) { - onRemovedAllNotifications(result.isSuccess) - } - } - - true - } - else -> false - } + // region View state + @VisibleForTesting + fun populateList(notifications: List) { + initializeAdapter() + adapter?.setNotificationItems(notifications) + binding?.run { + loadingContent.visibility = View.GONE + if (notifications.isNotEmpty()) { + swipeContainingEmpty.visibility = View.GONE + swipeContainingList.visibility = View.VISIBLE + } else { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.notifications_no_results_message) + ) + swipeContainingList.visibility = View.GONE + swipeContainingEmpty.visibility = View.VISIBLE } - }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } } private fun setLoadingMessage() { @@ -334,15 +269,29 @@ class NotificationsFragment : swipeContainingList.visibility = View.GONE loadingContent.visibility = View.GONE swipeContainingEmpty.visibility = View.VISIBLE - emptyList.emptyListView.visibility = View.VISIBLE - emptyList.emptyListViewHeadline.text = headline - emptyList.emptyListViewText.text = message - emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) - emptyList.emptyListViewText.visibility = View.VISIBLE - emptyList.emptyListIcon.visibility = View.VISIBLE + + emptyList.run { + emptyListView.visibility = View.VISIBLE + emptyListViewHeadline.text = headline + emptyListViewText.text = message + emptyListIcon.setImageResource(R.drawable.ic_notification) + emptyListViewText.visibility = View.VISIBLE + emptyListIcon.visibility = View.VISIBLE + } + } + } + + private fun showError() { + requireActivity().runOnUiThread { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.account_not_found) + ) } } + // endregion + // region callbacks override fun onRemovedNotification(isSuccess: Boolean) { if (!isSuccess) { DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.remove_notification_failed)) @@ -382,7 +331,11 @@ class NotificationsFragment : } } - override fun onActionCallback(isSuccess: Boolean, notification: Notification, holder: NotificationListAdapter.NotificationViewHolder) { + override fun onActionCallback( + isSuccess: Boolean, + notification: Notification, + holder: NotificationListAdapter.NotificationViewHolder + ) { if (isSuccess) { adapter?.removeNotification(holder) } else { @@ -390,6 +343,7 @@ class NotificationsFragment : DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.notification_action_failed)) } } + // endregion companion object { private val TAG = NotificationsFragment::class.java.simpleName diff --git a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt index d059721a4359..fae179bfa332 100644 --- a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt +++ b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt @@ -5,22 +5,19 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.notifications; +package com.owncloud.android.ui.notifications -import com.owncloud.android.lib.resources.notifications.models.Notification; -import com.owncloud.android.ui.adapter.NotificationListAdapter; - -public interface NotificationsContract { +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder +interface NotificationsContract { interface View { - void onRemovedNotification(boolean isSuccess); + fun onRemovedNotification(isSuccess: Boolean) - void removeNotification(NotificationListAdapter.NotificationViewHolder holder); + fun removeNotification(holder: NotificationViewHolder) - void onRemovedAllNotifications(boolean isSuccess); + fun onRemovedAllNotifications(isSuccess: Boolean) - void onActionCallback(boolean isSuccess, - Notification notification, - NotificationListAdapter.NotificationViewHolder holder); + fun onActionCallback(isSuccess: Boolean, notification: Notification, holder: NotificationViewHolder) } } From e0fe3d0eca703e568a09b01c14f43bcbce4c7e70 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 16:05:40 +0200 Subject: [PATCH 06/18] fix(navigator): fix notifications tests Signed-off-by: alperozturk96 --- ...cations.NotificationsFragmentIT_empty.png} | Bin ...ificationsFragmentIT_empty_dark_black.png} | Bin ...tificationsFragmentIT_empty_dark_blue.png} | Bin ...ificationsFragmentIT_empty_dark_white.png} | Bin ...ficationsFragmentIT_empty_light_black.png} | Bin ...ficationsFragmentIT_empty_light_white.png} | Bin ...cations.NotificationsFragmentIT_error.png} | Bin ...ificationsFragmentIT_error_dark_black.png} | Bin ...tificationsFragmentIT_error_dark_blue.png} | Bin ...ificationsFragmentIT_error_dark_white.png} | Bin ...ficationsFragmentIT_error_light_black.png} | Bin ...ficationsFragmentIT_error_light_white.png} | Bin ...ficationsFragmentIT_showNotifications.png} | Bin ...agmentIT_showNotifications_dark_black.png} | Bin ...ragmentIT_showNotifications_dark_blue.png} | Bin ...agmentIT_showNotifications_dark_white.png} | Bin ...gmentIT_showNotifications_light_black.png} | Bin ...gmentIT_showNotifications_light_white.png} | Bin .../ui/activity/NotificationsActivityIT.kt | 155 ---------------- .../ui/fragment/NotificationsFragmentIT.kt | 172 ++++++++++++++++++ .../ui/adapter/NotificationListAdapter.kt | 2 +- 21 files changed, 173 insertions(+), 156 deletions(-) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_blue.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_white.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_white.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_blue.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_white.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_white.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_blue.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_white.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_black.png} (100%) rename app/screenshots/generic/debug/{com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png => com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_white.png} (100%) delete mode 100644 app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt create mode 100644 app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_blue.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_dark_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_empty_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_blue.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_dark_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_error_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_blue.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_dark_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_black.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_black.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT_showNotifications_light_white.png diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt deleted file mode 100644 index acf1e541a109..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.activity - -import androidx.test.core.app.launchActivity -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isRoot -import com.owncloud.android.AbstractIT -import com.owncloud.android.lib.resources.notifications.models.Action -import com.owncloud.android.lib.resources.notifications.models.Notification -import com.owncloud.android.lib.resources.notifications.models.RichObject -import com.owncloud.android.utils.ScreenshotTest -import org.junit.Test -import java.util.GregorianCalendar - -class NotificationsActivityIT : AbstractIT() { - private val testClassName = "com.owncloud.android.ui.activity.NotificationsActivityIT" - - @Test - @ScreenshotTest - fun empty() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - sut.populateList(ArrayList()) - } - - val screenShotName = createName(testClassName + "_" + "empty", "") - onView(isRoot()).check(matches(isDisplayed())) - - scenario.onActivity { sut -> - screenshotViaName(sut, screenShotName) - } - } - } - - @Test - @ScreenshotTest - @SuppressWarnings("MagicNumber") - fun showNotifications() { - val date = GregorianCalendar() - date.set(2005, 4, 17, 10, 35, 30) // random date - - val notifications = ArrayList() - notifications.add( - Notification( - 1, - "files", - "user", - date.time, - "objectType", - "objectId", - "App recommendation: Tasks", - "SubjectRich", - HashMap(), - "Sync tasks from various devices with your Nextcloud and edit them online.", - "MessageRich", - HashMap(), - "link", - "icon", - ArrayList() - ) - ) - - val actions = ArrayList().apply { - add(Action("Send usage", "link", "url", true)) - add(Action("Not now", "link", "url", false)) - } - - notifications.add( - Notification( - 1, - "files", - "user", - date.time, - "objectType", - "objectId", - "Help improve Nextcloud", - "SubjectRich", - HashMap(), - "Do you want to help us to improve Nextcloud" + - " by providing some anonymize data about your setup and usage?", - "MessageRich", - HashMap(), - "link", - "icon", - actions - ) - ) - - val moreAction = ArrayList().apply { - add(Action("Send usage", "link", "url", true)) - add(Action("Not now", "link", "url", false)) - add(Action("third action", "link", "url", false)) - add(Action("Delay", "link", "url", false)) - } - - notifications.add( - Notification( - 2, - "files", - "user", - date.time, - "objectType", - "objectId", - "Help improve Nextcloud", - "SubjectRich", - HashMap(), - "Do you want to help us to improve Nextcloud by providing some anonymize data about your setup and " + - "usage?", - "MessageRich", - HashMap(), - "link", - "icon", - moreAction - ) - ) - - launchActivity().use { scenario -> - scenario.onActivity { sut -> - sut.populateList(notifications) - } - - val screenShotName = createName(testClassName + "_" + "showNotifications", "") - onView(isRoot()).check(matches(isDisplayed())) - - scenario.onActivity { sut -> - screenshotViaName(sut, screenShotName) - } - } - } - - @Test - @ScreenshotTest - fun error() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - sut.setEmptyContent("Error", "Error! Please try again later!") - } - - val screenShotName = createName(testClassName + "_" + "error", "") - onView(isRoot()).check(matches(isDisplayed())) - - scenario.onActivity { sut -> - screenshotViaName(sut, screenShotName) - } - } - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt new file mode 100644 index 000000000000..3682dfbec672 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -0,0 +1,172 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.fragment + +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.resources.notifications.models.Action +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen +import com.owncloud.android.utils.ScreenshotTest +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import java.util.Date +import java.util.GregorianCalendar + +class NotificationsFragmentIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.fragment.notifications.NotificationsFragmentIT" + + @get:Rule + var storagePermissionRules: TestRule = grant() + + private fun buildDate(): Date { + val cal = GregorianCalendar() + cal.set(2005, 4, 17, 10, 35, 30) + return cal.time + } + + private fun buildNotificationNoActions(): Notification = Notification( + 1, + "files", + "user", + buildDate(), + "objectType", + "objectId", + "App recommendation: Tasks", + "SubjectRich", + HashMap(), + "Sync tasks from various devices with your Nextcloud and edit them online.", + "MessageRich", + HashMap(), + "link", + "icon", + ArrayList() + ) + + private fun buildNotificationTwoActions(): Notification { + val actions = ArrayList() + actions.add(Action("Send usage", "link", "url", true)) + actions.add(Action("Not now", "link", "url", false)) + + return Notification( + 2, + "files", + "user", + buildDate(), + "objectType", + "objectId", + "Help improve Nextcloud", + "SubjectRich", + HashMap(), + "Do you want to help us to improve Nextcloud by providing some anonymize data about your setup and usage?", + "MessageRich", + HashMap(), + "link", + "icon", + actions + ) + } + + private fun buildNotificationManyActions(): Notification { + val actions = ArrayList() + actions.add(Action("Send usage", "link", "url", true)) + actions.add(Action("Not now", "link", "url", false)) + actions.add(Action("Third action", "link", "url", false)) + actions.add(Action("Delay", "link", "url", false)) + + return Notification( + 3, + "files", + "user", + buildDate(), + "objectType", + "objectId", + "Help improve Nextcloud", + "SubjectRich", + HashMap(), + "Do you want to help us to improve Nextcloud by providing some anonymize data about your setup and usage?", + "MessageRich", + HashMap(), + "link", + "icon", + actions + ) + } + + fun buildMockNotifications(): ArrayList = ArrayList().apply { + add(buildNotificationNoActions()) + add(buildNotificationTwoActions()) + add(buildNotificationManyActions()) + } + + private fun findFragment(sut: NavigatorActivity): NotificationsFragment? = sut.supportFragmentManager + .findFragmentByTag(NotificationsFragment::class.java.simpleName) as? NotificationsFragment + + @Test + @ScreenshotTest + fun empty() { + val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) + ActivityScenario.launch(intent).use { scenario -> + scenario.onActivity { sut -> + findFragment(sut)?.populateList(ArrayList()) + } + + val screenShotName = createName(testClassName + "_" + "empty", "") + onView(isRoot()).check(matches(isDisplayed())) + + scenario.onActivity { sut -> + screenshotViaName(sut, screenShotName) + } + } + } + + @Test + @ScreenshotTest + fun showNotifications() { + val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) + ActivityScenario.launch(intent).use { scenario -> + scenario.onActivity { sut -> + findFragment(sut)?.populateList(buildMockNotifications()) + } + + val screenShotName = createName(testClassName + "_" + "showNotifications", "") + onView(isRoot()).check(matches(isDisplayed())) + + scenario.onActivity { sut -> + screenshotViaName(sut, screenShotName) + } + } + } + + @Test + @ScreenshotTest + fun error() { + val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) + ActivityScenario.launch(intent).use { scenario -> + scenario.onActivity { sut -> + findFragment(sut)?.setEmptyContent("Error", "Error! Please try again later!") + } + + val screenShotName = createName(testClassName + "_" + "error", "") + onView(isRoot()).check(matches(isDisplayed())) + + scenario.onActivity { sut -> + screenshotViaName(sut, screenShotName) + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 65421e262301..74e0a2315a6d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -149,7 +149,7 @@ class NotificationListAdapter( ) } }.onFailure { e -> - Log_OC.e("RichDocumentsTemplateAdapter", "exception setData: $e") + Log_OC.e("NotificationListAdapter", "exception setData: $e") } } } From 191f1fb9ccb9941cb0e2213681fdcd047944f5ce Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 19:37:38 +0200 Subject: [PATCH 07/18] fix(navigator): fix getting account name from worker Signed-off-by: alperozturk96 --- .../ui/fragment/notifications/NotificationsFragment.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index aebe0f0176e5..1713b0fc625a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -98,10 +98,9 @@ class NotificationsFragment : // region Setup private fun initUser() { optionalUser = Optional.of(accountManager.user) - arguments?.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT)?.let { accountName -> - if (optionalUser?.get()?.accountName.equals(accountName, ignoreCase = true)) { - accountManager.setCurrentOwnCloudAccount(accountName) - } + val accountName = activity?.intent?.getStringExtra(NotificationWork.KEY_NOTIFICATION_ACCOUNT) + if (optionalUser?.get()?.accountName.equals(accountName, ignoreCase = true)) { + accountManager.setCurrentOwnCloudAccount(accountName) } } From e9744a88c53495fbc9154e9f090f242e686ed2c4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 19:48:58 +0200 Subject: [PATCH 08/18] fix(navigator): remove unnecessary ui run block Signed-off-by: alperozturk96 --- .../ui/fragment/notifications/NotificationsFragment.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 1713b0fc625a..ee0c861b6358 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -281,12 +281,10 @@ class NotificationsFragment : } private fun showError() { - requireActivity().runOnUiThread { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.account_not_found) - ) - } + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.account_not_found) + ) } // endregion From 6491900c1e0966e857b7b75999f52123a201c963 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Apr 2026 20:22:50 +0200 Subject: [PATCH 09/18] fix(navigator): remove unnecessary calls Signed-off-by: alperozturk96 --- .../ui/adapter/NotificationListAdapter.kt | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 74e0a2315a6d..c4f9471eeb58 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -38,7 +38,6 @@ import com.nextcloud.utils.GlideHelper import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.NotificationListItemBinding -import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation import com.owncloud.android.lib.resources.notifications.models.Action import com.owncloud.android.lib.resources.notifications.models.Notification @@ -74,12 +73,7 @@ class NotificationListAdapter( override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { val notification = notificationsList[position] - with(holder.binding) { - datetime.text = DisplayUtils.getRelativeTimestamp( - fragment.requireContext(), - notification.getDatetime().time - ) - } + bindDateTime(holder, notification) bindSubject(holder, notification) bindMessage(holder, notification) bindIcon(holder, notification) @@ -91,11 +85,19 @@ class NotificationListAdapter( // region Bind helpers + private fun bindDateTime(holder: NotificationViewHolder, notification: Notification) { + val timestamp = DisplayUtils.getRelativeTimestamp( + fragment.requireContext(), + notification.getDatetime().time + ) + holder.binding.datetime.text = timestamp + } + private fun bindSubject(holder: NotificationViewHolder, notification: Notification) { val file = notification.subjectRichParameters[FILE] if (file == null && !TextUtils.isEmpty(notification.getLink())) { val subject = "${notification.getSubject()} ↗" - holder.binding.subject.apply { + holder.binding.subject.run { setTypeface(typeface, Typeface.BOLD) text = subject setOnClickListener { @@ -103,7 +105,7 @@ class NotificationListAdapter( } } } else { - holder.binding.subject.apply { + holder.binding.subject.run { text = if (!TextUtils.isEmpty(notification.subjectRich)) { makeSpecialPartsBold(notification) } else { @@ -124,7 +126,7 @@ class NotificationListAdapter( private fun bindMessage(holder: NotificationViewHolder, notification: Notification) { val message = notification.getMessage() - holder.binding.message.apply { + holder.binding.message.run { if (!message.isNullOrEmpty()) { text = message visibility = View.VISIBLE @@ -136,22 +138,15 @@ class NotificationListAdapter( private fun bindIcon(holder: NotificationViewHolder, notification: Notification) { if (notification.getIcon().isNullOrEmpty()) return - fragment.lifecycleScope.launch(Dispatchers.IO) { - runCatching { - withContext(Dispatchers.Main) { - GlideHelper.loadIntoImageView( - fragment.requireContext(), - client, - notification.getIcon(), - holder.binding.icon, - R.drawable.ic_notification, - false - ) - } - }.onFailure { e -> - Log_OC.e("NotificationListAdapter", "exception setData: $e") - } - } + + GlideHelper.loadIntoImageView( + fragment.requireContext(), + client, + notification.getIcon(), + holder.binding.icon, + R.drawable.ic_notification, + false + ) } private fun colorViewHolder(holder: NotificationViewHolder) { @@ -171,13 +166,18 @@ class NotificationListAdapter( fun bindButtons(holder: NotificationViewHolder, notification: Notification) { holder.binding.dismiss.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { - val result = DeleteNotificationRemoteOperation(notification.notificationId).execute(client!!) - withContext(Dispatchers.Main) { fragment.onRemovedNotification(result.isSuccess) } + val result = + client?.let { clientValue -> + DeleteNotificationRemoteOperation(notification.notificationId).execute( + clientValue + ) + } + withContext(Dispatchers.Main) { fragment.onRemovedNotification(result?.isSuccess == true) } } } val actions = notification.getActions() - holder.binding.buttons.apply { + holder.binding.buttons.run { removeAllViews() setVisibleIf(actions.isNotEmpty()) if (actions.isEmpty()) return @@ -269,7 +269,7 @@ class NotificationListAdapter( Intent(Intent.ACTION_VIEW).apply { data = action.link?.toUri() } ) } else { - NotificationExecuteActionTask(client!!, holder, notification, fragment).execute(action) + client?.let { NotificationExecuteActionTask(it, holder, notification, fragment) }?.execute(action) } } @@ -323,23 +323,24 @@ class NotificationListAdapter( private fun makeSpecialPartsBold(notification: Notification): SpannableStringBuilder { var text = notification.getSubjectRich() val ssb = SpannableStringBuilder(text) + var openingBrace = text.indexOf('{') + var closingBrace: Int + var replaceablePart: String? while (openingBrace != -1) { - val closingBrace = text.indexOf('}', openingBrace) + 1 - val key = text.substring(openingBrace + 1, closingBrace - 1) - val richObject = notification.subjectRichParameters[key] - if (richObject != null) { - val name = richObject.name ?: "" + closingBrace = text.indexOf('}', openingBrace) + 1 + replaceablePart = text.substring(openingBrace + 1, closingBrace - 1) + notification.subjectRichParameters[replaceablePart]?.name?.let { name -> ssb.replace(openingBrace, closingBrace, name) text = ssb.toString() - val end = openingBrace + name.length - ssb.setSpan(styleSpanBold, openingBrace, end, 0) - ssb.setSpan(foregroundColorSpanBlack, openingBrace, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - openingBrace = text.indexOf('{', end) - } else { - openingBrace = text.indexOf('{', closingBrace) + closingBrace = openingBrace + name.length + + ssb.setSpan(styleSpanBold, openingBrace, closingBrace, 0) + ssb.setSpan(foregroundColorSpanBlack, openingBrace, closingBrace, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } + openingBrace = text.indexOf('{', closingBrace) } + return ssb } From ae88b6948985fd759d8f58848d1f09e552a3622d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Apr 2026 09:17:12 +0200 Subject: [PATCH 10/18] fix action bar padding Signed-off-by: alperozturk96 --- .../owncloud/android/ui/adapter/NotificationListAdapter.kt | 5 ++++- app/src/main/res/layout/notifications_layout.xml | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index c4f9471eeb58..0dbf24bebcda 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -180,7 +180,10 @@ class NotificationListAdapter( holder.binding.buttons.run { removeAllViews() setVisibleIf(actions.isNotEmpty()) - if (actions.isEmpty()) return + } + + if (actions.isEmpty()) { + return } val params = buttonLayoutParams() diff --git a/app/src/main/res/layout/notifications_layout.xml b/app/src/main/res/layout/notifications_layout.xml index 686a3bff8392..b9042e1ad700 100644 --- a/app/src/main/res/layout/notifications_layout.xml +++ b/app/src/main/res/layout/notifications_layout.xml @@ -18,7 +18,6 @@ From 7f4a56c8e2f41455c6983aed6245f0eb9e5299bd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Apr 2026 09:28:16 +0200 Subject: [PATCH 11/18] remove inner action bar since activity providing it Signed-off-by: alperozturk96 --- .../main/res/layout/notifications_layout.xml | 99 +++++++++---------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/app/src/main/res/layout/notifications_layout.xml b/app/src/main/res/layout/notifications_layout.xml index b9042e1ad700..34d790d4eef5 100644 --- a/app/src/main/res/layout/notifications_layout.xml +++ b/app/src/main/res/layout/notifications_layout.xml @@ -6,78 +6,71 @@ ~ SPDX-FileCopyrightText: 2017 Mario Danic ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - + android:layout_height="match_parent" + android:layout_below="@id/appbar" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - + android:footerDividersEnabled="false" + android:visibility="visible"> - - - - - + android:layout_marginLeft="-3dp" + android:layout_marginRight="-3dp" + android:layout_marginBottom="-3dp" + android:background="@color/bg_default" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + android:scrollbars="vertical" + android:visibility="visible" + tools:listitem="@layout/notification_list_item" /> + + + + - + android:layout_height="match_parent"> - - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:visibility="visible"> - + - + - + - + - + - + - - + + - - + \ No newline at end of file From d2037fa81a64ddbe08df4cc6f2c084a9cfc40201 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 15:41:28 +0200 Subject: [PATCH 12/18] fix codacy Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index b0ec486822e3..b57f29b1c836 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -648,7 +648,6 @@ class FileUploadHelper { files: List, accountName: String ): Pair, List> { - val autoUploadFolders = mutableListOf() val nonAutoUploadFiles = mutableListOf() From 4e58422797fa3efbae04015f4dd559a8cbff3ec8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 16:08:51 +0200 Subject: [PATCH 13/18] fix adapter initialization Signed-off-by: alperozturk96 --- .../ui/fragment/NotificationsFragmentIT.kt | 21 +++++- .../ui/adapter/NotificationListAdapter.kt | 16 ++-- .../notifications/NotificationsFragment.kt | 73 +++++++++---------- .../ui/notifications/NotificationsContract.kt | 3 +- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt index 3682dfbec672..dc69e5d8dcf0 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -8,11 +8,14 @@ */ package com.owncloud.android.ui.fragment +import android.net.Uri import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.platform.app.InstrumentationRegistry +import com.nextcloud.common.NextcloudClient import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.lib.resources.notifications.models.Action @@ -39,6 +42,15 @@ class NotificationsFragmentIT : AbstractIT() { return cal.time } + private val testClient: NextcloudClient by lazy { + NextcloudClient( + Uri.parse("https://cloud.example.com"), + "testuser", + "Basic dXNlcjpwYXNz", + targetContext + ) + } + private fun buildNotificationNoActions(): Notification = Notification( 1, "files", @@ -98,7 +110,8 @@ class NotificationsFragmentIT : AbstractIT() { "Help improve Nextcloud", "SubjectRich", HashMap(), - "Do you want to help us to improve Nextcloud by providing some anonymize data about your setup and usage?", + "Do you want to help us to improve Nextcloud by providing some anonymize data about your setup" + + " and usage?", "MessageRich", HashMap(), "link", @@ -122,9 +135,11 @@ class NotificationsFragmentIT : AbstractIT() { val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) ActivityScenario.launch(intent).use { scenario -> scenario.onActivity { sut -> - findFragment(sut)?.populateList(ArrayList()) + findFragment(sut)?.populateList(ArrayList(), testClient) } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val screenShotName = createName(testClassName + "_" + "empty", "") onView(isRoot()).check(matches(isDisplayed())) @@ -140,7 +155,7 @@ class NotificationsFragmentIT : AbstractIT() { val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) ActivityScenario.launch(intent).use { scenario -> scenario.onActivity { sut -> - findFragment(sut)?.populateList(buildMockNotifications()) + findFragment(sut)?.populateList(buildMockNotifications(), testClient) } val screenShotName = createName(testClassName + "_" + "showNotifications", "") diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 0dbf24bebcda..46290391b2bf 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.withContext @Suppress("TooManyFunctions") class NotificationListAdapter( - private val client: NextcloudClient?, + private val client: NextcloudClient, private val fragment: NotificationsFragment, private val viewThemeUtils: ViewThemeUtils ) : RecyclerView.Adapter() { @@ -167,12 +167,12 @@ class NotificationListAdapter( holder.binding.dismiss.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { val result = - client?.let { clientValue -> - DeleteNotificationRemoteOperation(notification.notificationId).execute( - clientValue - ) - } - withContext(Dispatchers.Main) { fragment.onRemovedNotification(result?.isSuccess == true) } + DeleteNotificationRemoteOperation(notification.notificationId).execute( + client + ) + withContext(Dispatchers.Main) { + fragment.onRemovedNotification(result?.isSuccess == true, client) + } } } @@ -272,7 +272,7 @@ class NotificationListAdapter( Intent(Intent.ACTION_VIEW).apply { data = action.link?.toUri() } ) } else { - client?.let { NotificationExecuteActionTask(it, holder, notification, fragment) }?.execute(action) + NotificationExecuteActionTask(client, holder, notification, fragment).execute(action) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index ee0c861b6358..cddd17d154ca 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -29,7 +29,6 @@ import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.BuildHelper @@ -62,15 +61,12 @@ class NotificationsFragment : private var binding: NotificationsLayoutBinding? = null private var adapter: NotificationListAdapter? = null private var snackbar: Snackbar? = null - private var client: NextcloudClient? = null private var optionalUser: Optional? = null @Inject lateinit var viewThemeUtils: ViewThemeUtils @Inject lateinit var accountManager: UserAccountManager - @Inject lateinit var clientFactory: ClientFactory - @Inject lateinit var preferences: AppPreferences // region Lifecycle @@ -81,12 +77,23 @@ class NotificationsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupMenu() - initUser() - setupSwipeRefresh() - setupPushWarning() - setupContent() - if (optionalUser?.isPresent == false) showError() + + lifecycleScope.launch { + val baseActivity = getTypedActivity(BaseActivity::class.java) + val client = baseActivity?.clientRepository?.getNextcloudClient() ?: run { + showError() + return@launch + } + + withContext(Dispatchers.Main) { + setupMenu(client) + initUser() + setupSwipeRefresh(client) + setupPushWarning() + setupContent(client) + if (optionalUser?.isPresent == false) showError() + } + } } override fun onDestroyView() { @@ -104,32 +111,32 @@ class NotificationsFragment : } } - private fun setupSwipeRefresh() { + private fun setupSwipeRefresh(client: NextcloudClient) { binding?.run { viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingList) viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingEmpty) swipeContainingList.setOnRefreshListener { setLoadingMessage() swipeContainingList.isRefreshing = true - fetchAndSetData() + fetchAndSetData(client) } swipeContainingEmpty.setOnRefreshListener { setLoadingMessageEmpty() - fetchAndSetData() + fetchAndSetData(client) } } } - private fun setupContent() { + private fun setupContent(client: NextcloudClient) { binding?.run { emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) setLoadingMessageEmpty() list.layoutManager = LinearLayoutManager(requireContext()) - fetchAndSetData() + fetchAndSetData(client) } } - private fun setupMenu() { + private fun setupMenu(client: NextcloudClient) { (requireActivity() as MenuHost).addMenuProvider( object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { @@ -139,7 +146,7 @@ class NotificationsFragment : override fun onMenuItemSelected(item: MenuItem): Boolean { if (item.itemId != R.id.action_empty_notifications) return false lifecycleScope.launch(Dispatchers.IO) { - val result = DeleteAllNotificationsRemoteOperation().execute(client!!) + val result = DeleteAllNotificationsRemoteOperation().execute(client) withContext(Dispatchers.Main) { onRemovedAllNotifications(result.isSuccess) } } return true @@ -186,13 +193,13 @@ class NotificationsFragment : // endregion // region Data loading - private fun fetchAndSetData() { + private fun fetchAndSetData(client: NextcloudClient) { lifecycleScope.launch(Dispatchers.IO) { - initializeAdapter() - val result = client?.let { GetNotificationsRemoteOperation().execute(it) } + initializeAdapter(client) + val result = GetNotificationsRemoteOperation().execute(client) withContext(Dispatchers.Main) { if (result?.isSuccess == true && result.resultData != null) { - populateList(result.resultData ?: listOf()) + populateList(result.resultData ?: listOf(), client) } else { try { Log_OC.d(TAG, result?.logMessage) @@ -208,17 +215,10 @@ class NotificationsFragment : } } - private fun initializeAdapter() { - lifecycleScope.launch { - val baseActivity = getTypedActivity(BaseActivity::class.java) - client = baseActivity?.clientRepository?.getNextcloudClient() - - withContext(Dispatchers.Main) { - if (adapter == null) { - adapter = NotificationListAdapter(client, this@NotificationsFragment, viewThemeUtils) - binding?.list?.adapter = adapter - } - } + private fun initializeAdapter(client: NextcloudClient) { + if (adapter == null) { + adapter = NotificationListAdapter(client, this@NotificationsFragment, viewThemeUtils) + binding?.list?.adapter = adapter } } @@ -229,9 +229,8 @@ class NotificationsFragment : // endregion // region View state - @VisibleForTesting - fun populateList(notifications: List) { - initializeAdapter() + fun populateList(notifications: List, client: NextcloudClient) { + initializeAdapter(client) adapter?.setNotificationItems(notifications) binding?.run { loadingContent.visibility = View.GONE @@ -289,10 +288,10 @@ class NotificationsFragment : // endregion // region callbacks - override fun onRemovedNotification(isSuccess: Boolean) { + override fun onRemovedNotification(isSuccess: Boolean, client: NextcloudClient) { if (!isSuccess) { DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.remove_notification_failed)) - fetchAndSetData() + fetchAndSetData(client) } } diff --git a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt index fae179bfa332..01702e1ed0ef 100644 --- a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt +++ b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.kt @@ -7,12 +7,13 @@ */ package com.owncloud.android.ui.notifications +import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder interface NotificationsContract { interface View { - fun onRemovedNotification(isSuccess: Boolean) + fun onRemovedNotification(isSuccess: Boolean, client: NextcloudClient) fun removeNotification(holder: NotificationViewHolder) From 1e5ad690d213203219d91ca6d4279f2fc163598c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Apr 2026 12:16:28 +0200 Subject: [PATCH 14/18] de-couple nextcloud client from adapter Signed-off-by: alperozturk96 --- .../ui/fragment/NotificationsFragmentIT.kt | 15 +---- .../ui/adapter/NotificationListAdapter.kt | 36 ++--------- .../NotificationsAdapterItemClick.kt | 23 +++++++ .../notifications/NotificationsFragment.kt | 62 +++++++++++++++++-- 4 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsAdapterItemClick.kt diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt index dc69e5d8dcf0..50f89c0cb259 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -8,14 +8,12 @@ */ package com.owncloud.android.ui.fragment -import android.net.Uri import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.platform.app.InstrumentationRegistry -import com.nextcloud.common.NextcloudClient import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.lib.resources.notifications.models.Action @@ -42,15 +40,6 @@ class NotificationsFragmentIT : AbstractIT() { return cal.time } - private val testClient: NextcloudClient by lazy { - NextcloudClient( - Uri.parse("https://cloud.example.com"), - "testuser", - "Basic dXNlcjpwYXNz", - targetContext - ) - } - private fun buildNotificationNoActions(): Notification = Notification( 1, "files", @@ -135,7 +124,7 @@ class NotificationsFragmentIT : AbstractIT() { val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) ActivityScenario.launch(intent).use { scenario -> scenario.onActivity { sut -> - findFragment(sut)?.populateList(ArrayList(), testClient) + findFragment(sut)?.populateList(ArrayList()) } InstrumentationRegistry.getInstrumentation().waitForIdleSync() @@ -155,7 +144,7 @@ class NotificationsFragmentIT : AbstractIT() { val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) ActivityScenario.launch(intent).use { scenario -> scenario.onActivity { sut -> - findFragment(sut)?.populateList(buildMockNotifications(), testClient) + findFragment(sut)?.populateList(buildMockNotifications()) } val screenShotName = createName(testClassName + "_" + "showNotifications", "") diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index 46290391b2bf..c5249217b7a2 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -29,32 +29,25 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.net.toUri import androidx.core.view.size -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.common.NextcloudClient -import com.nextcloud.utils.GlideHelper import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.NotificationListItemBinding -import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation import com.owncloud.android.lib.resources.notifications.models.Action import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.asynctasks.NotificationExecuteActionTask +import com.owncloud.android.ui.fragment.notifications.NotificationsAdapterItemClick import com.owncloud.android.ui.fragment.notifications.NotificationsFragment import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.ViewThemeUtils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Suppress("TooManyFunctions") class NotificationListAdapter( - private val client: NextcloudClient, private val fragment: NotificationsFragment, - private val viewThemeUtils: ViewThemeUtils + private val viewThemeUtils: ViewThemeUtils, + private val itemClick: NotificationsAdapterItemClick ) : RecyclerView.Adapter() { private val styleSpanBold = StyleSpan(Typeface.BOLD) @@ -138,15 +131,7 @@ class NotificationListAdapter( private fun bindIcon(holder: NotificationViewHolder, notification: Notification) { if (notification.getIcon().isNullOrEmpty()) return - - GlideHelper.loadIntoImageView( - fragment.requireContext(), - client, - notification.getIcon(), - holder.binding.icon, - R.drawable.ic_notification, - false - ) + itemClick.onBindIcon(holder.binding.icon, notification.getIcon()) } private fun colorViewHolder(holder: NotificationViewHolder) { @@ -162,18 +147,9 @@ class NotificationListAdapter( // endregion // region Button binding - fun bindButtons(holder: NotificationViewHolder, notification: Notification) { holder.binding.dismiss.setOnClickListener { - fragment.lifecycleScope.launch(Dispatchers.IO) { - val result = - DeleteNotificationRemoteOperation(notification.notificationId).execute( - client - ) - withContext(Dispatchers.Main) { - fragment.onRemovedNotification(result?.isSuccess == true, client) - } - } + itemClick.deleteNotification(notification.notificationId) } val actions = notification.getActions() @@ -272,7 +248,7 @@ class NotificationListAdapter( Intent(Intent.ACTION_VIEW).apply { data = action.link?.toUri() } ) } else { - NotificationExecuteActionTask(client, holder, notification, fragment).execute(action) + itemClick.onActionClick(holder, action, notification) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsAdapterItemClick.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsAdapterItemClick.kt new file mode 100644 index 000000000000..0329f2b2b482 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsAdapterItemClick.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment.notifications + +import android.widget.ImageView +import com.owncloud.android.lib.resources.notifications.models.Action +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.adapter.NotificationListAdapter + +interface NotificationsAdapterItemClick { + fun onBindIcon(imageView: ImageView, url: String) + fun deleteNotification(id: Int) + fun onActionClick( + holder: NotificationListAdapter.NotificationViewHolder, + action: Action, + notification: Notification + ) +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index cddd17d154ca..4c6d6e867d91 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -17,6 +17,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.annotation.VisibleForTesting import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -32,16 +33,20 @@ import com.nextcloud.client.jobs.NotificationWork import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.BuildHelper +import com.nextcloud.utils.GlideHelper import com.nextcloud.utils.extensions.getTypedActivity import com.owncloud.android.R import com.owncloud.android.databinding.NotificationsLayoutBinding import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.notifications.DeleteAllNotificationsRemoteOperation +import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation +import com.owncloud.android.lib.resources.notifications.models.Action import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.adapter.NotificationListAdapter +import com.owncloud.android.ui.asynctasks.NotificationExecuteActionTask import com.owncloud.android.ui.notifications.NotificationsContract import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PushUtils @@ -56,6 +61,7 @@ import javax.inject.Inject class NotificationsFragment : Fragment(), NotificationsContract.View, + NotificationsAdapterItemClick, Injectable { private var binding: NotificationsLayoutBinding? = null @@ -69,6 +75,8 @@ class NotificationsFragment : @Inject lateinit var preferences: AppPreferences + private var client: NextcloudClient? = null + // region Lifecycle override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = NotificationsLayoutBinding.inflate(inflater, container, false) @@ -84,6 +92,7 @@ class NotificationsFragment : showError() return@launch } + this@NotificationsFragment.client = client withContext(Dispatchers.Main) { setupMenu(client) @@ -195,11 +204,11 @@ class NotificationsFragment : // region Data loading private fun fetchAndSetData(client: NextcloudClient) { lifecycleScope.launch(Dispatchers.IO) { - initializeAdapter(client) + initializeAdapter() val result = GetNotificationsRemoteOperation().execute(client) withContext(Dispatchers.Main) { if (result?.isSuccess == true && result.resultData != null) { - populateList(result.resultData ?: listOf(), client) + populateList(result.resultData ?: listOf()) } else { try { Log_OC.d(TAG, result?.logMessage) @@ -215,9 +224,9 @@ class NotificationsFragment : } } - private fun initializeAdapter(client: NextcloudClient) { + private fun initializeAdapter() { if (adapter == null) { - adapter = NotificationListAdapter(client, this@NotificationsFragment, viewThemeUtils) + adapter = NotificationListAdapter(this@NotificationsFragment, viewThemeUtils, this) binding?.list?.adapter = adapter } } @@ -229,8 +238,8 @@ class NotificationsFragment : // endregion // region View state - fun populateList(notifications: List, client: NextcloudClient) { - initializeAdapter(client) + fun populateList(notifications: List) { + initializeAdapter() adapter?.setNotificationItems(notifications) binding?.run { loadingContent.visibility = View.GONE @@ -339,6 +348,47 @@ class NotificationsFragment : DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.notification_action_failed)) } } + + override fun onBindIcon(imageView: ImageView, url: String) { + GlideHelper.loadIntoImageView( + requireContext(), + client, + url, + imageView, + R.drawable.ic_notification, + false + ) + } + + override fun deleteNotification(id: Int) { + val client = client ?: run { + Log_OC.e(TAG, "client not initialized") + return + } + + lifecycleScope.launch(Dispatchers.IO) { + val result = + DeleteNotificationRemoteOperation(id).execute( + client + ) + withContext(Dispatchers.Main) { + onRemovedNotification(result?.isSuccess == true, client) + } + } + } + + override fun onActionClick( + holder: NotificationListAdapter.NotificationViewHolder, + action: Action, + notification: Notification + ) { + val client = client ?: run { + Log_OC.e(TAG, "client not initialized") + return + } + + NotificationExecuteActionTask(client, holder, notification, this).execute(action) + } // endregion companion object { From 7109bf2016408128c3193660829f484b4a93dae5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Apr 2026 13:06:23 +0200 Subject: [PATCH 15/18] simplify Signed-off-by: alperozturk96 --- .../ui/fragment/NotificationsFragmentIT.kt | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt index 50f89c0cb259..48ce3df00cd4 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -13,7 +13,6 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot -import androidx.test.platform.app.InstrumentationRegistry import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.lib.resources.notifications.models.Action @@ -34,11 +33,9 @@ class NotificationsFragmentIT : AbstractIT() { @get:Rule var storagePermissionRules: TestRule = grant() - private fun buildDate(): Date { - val cal = GregorianCalendar() - cal.set(2005, 4, 17, 10, 35, 30) - return cal.time - } + private fun buildDate(): Date = GregorianCalendar().apply { + set(2005, 4, 17, 10, 35, 30) + }.time private fun buildNotificationNoActions(): Notification = Notification( 1, @@ -59,9 +56,10 @@ class NotificationsFragmentIT : AbstractIT() { ) private fun buildNotificationTwoActions(): Notification { - val actions = ArrayList() - actions.add(Action("Send usage", "link", "url", true)) - actions.add(Action("Not now", "link", "url", false)) + val actions = ArrayList().apply { + add(Action("Send usage", "link", "url", true)) + add(Action("Not now", "link", "url", false)) + } return Notification( 2, @@ -83,11 +81,12 @@ class NotificationsFragmentIT : AbstractIT() { } private fun buildNotificationManyActions(): Notification { - val actions = ArrayList() - actions.add(Action("Send usage", "link", "url", true)) - actions.add(Action("Not now", "link", "url", false)) - actions.add(Action("Third action", "link", "url", false)) - actions.add(Action("Delay", "link", "url", false)) + val actions = ArrayList().apply { + add(Action("Send usage", "link", "url", true)) + add(Action("Not now", "link", "url", false)) + add(Action("Third action", "link", "url", false)) + add(Action("Delay", "link", "url", false)) + } return Notification( 3, @@ -116,7 +115,7 @@ class NotificationsFragmentIT : AbstractIT() { } private fun findFragment(sut: NavigatorActivity): NotificationsFragment? = sut.supportFragmentManager - .findFragmentByTag(NotificationsFragment::class.java.simpleName) as? NotificationsFragment + .findFragmentByTag(NavigatorScreen.Notifications.tag) as? NotificationsFragment @Test @ScreenshotTest @@ -127,8 +126,6 @@ class NotificationsFragmentIT : AbstractIT() { findFragment(sut)?.populateList(ArrayList()) } - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - val screenShotName = createName(testClassName + "_" + "empty", "") onView(isRoot()).check(matches(isDisplayed())) From b6e98972a75444cb0ca1fa98258597ac6f94fa38 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Apr 2026 17:09:53 +0200 Subject: [PATCH 16/18] fix ss tests Signed-off-by: alperozturk96 --- .../ui/fragment/NotificationsFragmentIT.kt | 60 +++-- .../notifications/NotificationsFragment.kt | 225 ++++++++++-------- .../model/NotificationsUIState.kt | 17 ++ .../main/res/layout/notifications_layout.xml | 12 +- 4 files changed, 174 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/notifications/model/NotificationsUIState.kt diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt index 48ce3df00cd4..1c3634ba9cab 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -18,6 +18,7 @@ import com.owncloud.android.AbstractIT import com.owncloud.android.lib.resources.notifications.models.Action import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import com.owncloud.android.ui.fragment.notifications.model.NotificationsUIState import com.owncloud.android.ui.navigation.NavigatorActivity import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.utils.ScreenshotTest @@ -114,21 +115,32 @@ class NotificationsFragmentIT : AbstractIT() { add(buildNotificationManyActions()) } - private fun findFragment(sut: NavigatorActivity): NotificationsFragment? = sut.supportFragmentManager - .findFragmentByTag(NavigatorScreen.Notifications.tag) as? NotificationsFragment + private fun findFragment(sut: NavigatorActivity): NotificationsFragment? { + val allFragments = sut.supportFragmentManager.fragments + for (f in allFragments) { + if (f is NotificationsFragment) return f + val child = f.childFragmentManager.fragments.filterIsInstance().firstOrNull() + if (child != null) return child + } + return null + } - @Test - @ScreenshotTest - fun empty() { + private fun launchFragment(name: String, block: NotificationsFragment.() -> Unit) { val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) + .putExtra(NotificationsFragment.EXTRA_INIT_FOR_TESTING, true) + ActivityScenario.launch(intent).use { scenario -> + onView(isRoot()).check(matches(isDisplayed())) + scenario.onActivity { sut -> - findFragment(sut)?.populateList(ArrayList()) + val fragment = findFragment(sut) + ?: throw IllegalStateException("NotificationsFragment not found in NavigatorActivity!") + fragment.block() } - val screenShotName = createName(testClassName + "_" + "empty", "") onView(isRoot()).check(matches(isDisplayed())) + val screenShotName = createName(testClassName + "_" + name, "") scenario.onActivity { sut -> screenshotViaName(sut, screenShotName) } @@ -137,37 +149,23 @@ class NotificationsFragmentIT : AbstractIT() { @Test @ScreenshotTest - fun showNotifications() { - val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) - ActivityScenario.launch(intent).use { scenario -> - scenario.onActivity { sut -> - findFragment(sut)?.populateList(buildMockNotifications()) - } - - val screenShotName = createName(testClassName + "_" + "showNotifications", "") - onView(isRoot()).check(matches(isDisplayed())) + fun empty() { + launchFragment("empty") { initForTesting(NotificationsUIState.Empty) } + } - scenario.onActivity { sut -> - screenshotViaName(sut, screenShotName) - } + @Test + @ScreenshotTest + fun showNotifications() { + launchFragment("showNotifications") { + initForTesting(NotificationsUIState.Loaded(buildMockNotifications())) } } @Test @ScreenshotTest fun error() { - val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Notifications) - ActivityScenario.launch(intent).use { scenario -> - scenario.onActivity { sut -> - findFragment(sut)?.setEmptyContent("Error", "Error! Please try again later!") - } - - val screenShotName = createName(testClassName + "_" + "error", "") - onView(isRoot()).check(matches(isDisplayed())) - - scenario.onActivity { sut -> - screenshotViaName(sut, screenShotName) - } + launchFragment("error") { + initForTesting(NotificationsUIState.Error("Error! Please try again later!")) } } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 4c6d6e867d91..7b276ed708ec 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -47,6 +47,7 @@ import com.owncloud.android.lib.resources.notifications.models.Notification import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.adapter.NotificationListAdapter import com.owncloud.android.ui.asynctasks.NotificationExecuteActionTask +import com.owncloud.android.ui.fragment.notifications.model.NotificationsUIState import com.owncloud.android.ui.notifications.NotificationsContract import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PushUtils @@ -69,14 +70,23 @@ class NotificationsFragment : private var snackbar: Snackbar? = null private var optionalUser: Optional? = null - @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var viewThemeUtils: ViewThemeUtils - @Inject lateinit var accountManager: UserAccountManager + @Inject + lateinit var accountManager: UserAccountManager - @Inject lateinit var preferences: AppPreferences + @Inject + lateinit var preferences: AppPreferences private var client: NextcloudClient? = null + private var state: NotificationsUIState = NotificationsUIState.Loading + set(value) { + field = value + renderState(value) + } + // region Lifecycle override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = NotificationsLayoutBinding.inflate(inflater, container, false) @@ -85,11 +95,13 @@ class NotificationsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val initForTesting = activity?.intent?.getBooleanExtra(EXTRA_INIT_FOR_TESTING, false) + if (initForTesting == true) return lifecycleScope.launch { val baseActivity = getTypedActivity(BaseActivity::class.java) val client = baseActivity?.clientRepository?.getNextcloudClient() ?: run { - showError() + state = NotificationsUIState.Error(getString(R.string.account_not_found)) return@launch } this@NotificationsFragment.client = client @@ -100,7 +112,9 @@ class NotificationsFragment : setupSwipeRefresh(client) setupPushWarning() setupContent(client) - if (optionalUser?.isPresent == false) showError() + if (optionalUser?.isPresent == false) { + state = NotificationsUIState.Error(getString(R.string.account_not_found)) + } } } } @@ -111,6 +125,69 @@ class NotificationsFragment : } // endregion + // region State rendering + + private fun renderState(state: NotificationsUIState) { + binding?.run { + itemSwipeRefreshLayout.visibility = View.GONE + shimmerAndEmptySwipeRefreshLayout.visibility = View.GONE + + shimmerLayout.visibility = View.GONE + emptyList.root.visibility = View.GONE + + emptyList.emptyListViewHeadline.text = "" + emptyList.emptyListViewText.text = "" + emptyList.emptyListViewText.visibility = View.GONE + + when (state) { + is NotificationsUIState.Loading -> renderLoading() + + is NotificationsUIState.Loaded -> renderLoaded(state.items) + + is NotificationsUIState.Empty -> renderEmpty( + headline = getString(R.string.notifications_no_results_headline), + message = getString(R.string.notifications_no_results_message) + ) + + is NotificationsUIState.Error -> renderEmpty( + headline = getString(R.string.notifications_no_results_headline), + message = state.message + ) + } + } + } + + private fun NotificationsLayoutBinding.renderEmpty(headline: String, message: String?) { + shimmerAndEmptySwipeRefreshLayout.visibility = View.VISIBLE + + shimmerLayout.visibility = View.GONE + + emptyList.run { + root.visibility = View.VISIBLE + emptyListIcon.visibility = View.VISIBLE + + emptyListViewHeadline.text = headline + emptyListIcon.setImageResource(R.drawable.ic_notification) + emptyListViewText.apply { + text = message ?: "" + visibility = if (message.isNullOrEmpty()) View.GONE else View.VISIBLE + } + } + } + + private fun renderLoading() { + binding?.shimmerAndEmptySwipeRefreshLayout?.visibility = View.VISIBLE + binding?.shimmerLayout?.visibility = View.VISIBLE + binding?.emptyList?.root?.visibility = View.GONE + } + + private fun renderLoaded(items: List) { + initializeAdapter() + adapter?.setNotificationItems(items) + binding?.itemSwipeRefreshLayout?.visibility = View.VISIBLE + } + // endregion + // region Setup private fun initUser() { optionalUser = Optional.of(accountManager.user) @@ -122,15 +199,15 @@ class NotificationsFragment : private fun setupSwipeRefresh(client: NextcloudClient) { binding?.run { - viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingList) - viewThemeUtils.androidx.themeSwipeRefreshLayout(swipeContainingEmpty) - swipeContainingList.setOnRefreshListener { - setLoadingMessage() - swipeContainingList.isRefreshing = true + viewThemeUtils.androidx.themeSwipeRefreshLayout(itemSwipeRefreshLayout) + viewThemeUtils.androidx.themeSwipeRefreshLayout(shimmerAndEmptySwipeRefreshLayout) + itemSwipeRefreshLayout.setOnRefreshListener { + state = NotificationsUIState.Loading + itemSwipeRefreshLayout.isRefreshing = true fetchAndSetData(client) } - swipeContainingEmpty.setOnRefreshListener { - setLoadingMessageEmpty() + shimmerAndEmptySwipeRefreshLayout.setOnRefreshListener { + state = NotificationsUIState.Loading fetchAndSetData(client) } } @@ -139,7 +216,6 @@ class NotificationsFragment : private fun setupContent(client: NextcloudClient) { binding?.run { emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) - setLoadingMessageEmpty() list.layoutManager = LinearLayoutManager(requireContext()) fetchAndSetData(client) } @@ -207,16 +283,26 @@ class NotificationsFragment : initializeAdapter() val result = GetNotificationsRemoteOperation().execute(client) withContext(Dispatchers.Main) { - if (result?.isSuccess == true && result.resultData != null) { - populateList(result.resultData ?: listOf()) - } else { - try { - Log_OC.d(TAG, result?.logMessage) - setEmptyContent( - getString(R.string.notifications_no_results_headline), - result?.getLogMessage(requireContext()) - ) - } catch (_: Exception) { + state = when { + result?.isSuccess == true && result.resultData != null -> { + val items = result.resultData ?: emptyList() + if (items.isEmpty()) { + NotificationsUIState.Empty + } else { + NotificationsUIState.Loaded(items) + } + } + + else -> { + try { + Log_OC.d(TAG, result?.logMessage) + NotificationsUIState.Error( + result?.getLogMessage(requireContext()) + ?: getString(R.string.notifications_no_results_message) + ) + } catch (_: Exception) { + NotificationsUIState.Error(getString(R.string.notifications_no_results_message)) + } } } hideRefreshLayoutLoader() @@ -232,71 +318,20 @@ class NotificationsFragment : } private fun hideRefreshLayoutLoader() { - binding?.swipeContainingList?.isRefreshing = false - binding?.swipeContainingEmpty?.isRefreshing = false + binding?.itemSwipeRefreshLayout?.isRefreshing = false + binding?.shimmerAndEmptySwipeRefreshLayout?.isRefreshing = false } // endregion - // region View state - fun populateList(notifications: List) { - initializeAdapter() - adapter?.setNotificationItems(notifications) - binding?.run { - loadingContent.visibility = View.GONE - if (notifications.isNotEmpty()) { - swipeContainingEmpty.visibility = View.GONE - swipeContainingList.visibility = View.VISIBLE - } else { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ) - swipeContainingList.visibility = View.GONE - swipeContainingEmpty.visibility = View.VISIBLE - } - } - } - - private fun setLoadingMessage() { - binding?.swipeContainingEmpty?.visibility = View.GONE - } - - @VisibleForTesting - fun setLoadingMessageEmpty() { - binding?.run { - swipeContainingList.visibility = View.GONE - emptyList.emptyListView.visibility = View.GONE - loadingContent.visibility = View.VISIBLE - } - } - @VisibleForTesting - fun setEmptyContent(headline: String?, message: String?) { - binding?.run { - swipeContainingList.visibility = View.GONE - loadingContent.visibility = View.GONE - swipeContainingEmpty.visibility = View.VISIBLE - - emptyList.run { - emptyListView.visibility = View.VISIBLE - emptyListViewHeadline.text = headline - emptyListViewText.text = message - emptyListIcon.setImageResource(R.drawable.ic_notification) - emptyListViewText.visibility = View.VISIBLE - emptyListIcon.visibility = View.VISIBLE - } - } + fun initForTesting(state: NotificationsUIState) { + adapter = NotificationListAdapter(this@NotificationsFragment, viewThemeUtils, this) + binding?.list?.adapter = adapter + binding?.list?.layoutManager = LinearLayoutManager(requireContext()) + this.state = state } - private fun showError() { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.account_not_found) - ) - } - // endregion - - // region callbacks + // region Callbacks override fun onRemovedNotification(isSuccess: Boolean, client: NextcloudClient) { if (!isSuccess) { DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.remove_notification_failed)) @@ -307,30 +342,14 @@ class NotificationsFragment : override fun removeNotification(holder: NotificationListAdapter.NotificationViewHolder) { adapter?.removeNotification(holder) if (adapter?.itemCount == 0) { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ) - binding?.run { - swipeContainingList.visibility = View.GONE - loadingContent.visibility = View.GONE - swipeContainingEmpty.visibility = View.VISIBLE - } + state = NotificationsUIState.Empty } } override fun onRemovedAllNotifications(isSuccess: Boolean) { if (isSuccess) { adapter?.removeAllNotifications() - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ) - binding?.run { - loadingContent.visibility = View.GONE - swipeContainingList.visibility = View.GONE - swipeContainingEmpty.visibility = View.VISIBLE - } + state = NotificationsUIState.Empty } else { DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.clear_notifications_failed)) } @@ -367,10 +386,7 @@ class NotificationsFragment : } lifecycleScope.launch(Dispatchers.IO) { - val result = - DeleteNotificationRemoteOperation(id).execute( - client - ) + val result = DeleteNotificationRemoteOperation(id).execute(client) withContext(Dispatchers.Main) { onRemovedNotification(result?.isSuccess == true, client) } @@ -393,5 +409,6 @@ class NotificationsFragment : companion object { private val TAG = NotificationsFragment::class.java.simpleName + const val EXTRA_INIT_FOR_TESTING = "init_for_testing" } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/model/NotificationsUIState.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/model/NotificationsUIState.kt new file mode 100644 index 000000000000..500472318a02 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/model/NotificationsUIState.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment.notifications.model + +import com.owncloud.android.lib.resources.notifications.models.Notification + +sealed class NotificationsUIState { + data object Loading : NotificationsUIState() + data object Empty : NotificationsUIState() + data class Loaded(val items: List) : NotificationsUIState() + data class Error(val message: String?) : NotificationsUIState() +} diff --git a/app/src/main/res/layout/notifications_layout.xml b/app/src/main/res/layout/notifications_layout.xml index 34d790d4eef5..544180b8dd68 100644 --- a/app/src/main/res/layout/notifications_layout.xml +++ b/app/src/main/res/layout/notifications_layout.xml @@ -16,7 +16,7 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior"> + android:layout_height="match_parent" + android:visibility="visible"> - \ No newline at end of file + From 624b0ab7a0930b7710ba3fcf02cf2eec4067885c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 13 Apr 2026 09:54:44 +0200 Subject: [PATCH 17/18] simplify logic Signed-off-by: alperozturk96 --- .../ui/fragment/NotificationsFragmentIT.kt | 1 + .../android/ui/adapter/UserInfoAdapter.kt | 21 +++++++++++-------- .../notifications/NotificationsFragment.kt | 11 +--------- .../ui/navigation/NavigatorActivity.kt | 9 ++++++-- .../main/res/layout/notifications_layout.xml | 6 ++---- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt index 1c3634ba9cab..6959dcdc4d85 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/NotificationsFragmentIT.kt @@ -115,6 +115,7 @@ class NotificationsFragmentIT : AbstractIT() { add(buildNotificationManyActions()) } + @Suppress("ReturnCount") private fun findFragment(sut: NavigatorActivity): NotificationsFragment? { val allFragments = sut.supportFragmentManager.fragments for (f in allFragments) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UserInfoAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UserInfoAdapter.kt index 99b2564d1a9b..9c4769bc774d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UserInfoAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UserInfoAdapter.kt @@ -20,8 +20,11 @@ import com.owncloud.android.databinding.UserInfoDetailsTableItemBinding import com.owncloud.android.databinding.UserInfoDetailsTableItemTitleBinding import com.owncloud.android.utils.theme.ViewThemeUtils -class UserInfoAdapter(val context: Context, val displayList: Map>, val viewThemeUtils: ViewThemeUtils) : - SectionedRecyclerViewAdapter() { +class UserInfoAdapter( + val context: Context, + val displayList: Map>, + val viewThemeUtils: ViewThemeUtils +) : SectionedRecyclerViewAdapter() { companion object { const val SECTION_USERINFO = 0 const val SECTION_GROUPS = 1 @@ -47,17 +50,21 @@ class UserInfoAdapter(val context: Context, val displayList: Map UserInfoSectionedViewHolder( UserInfoDetailsTableItemBinding.inflate( LayoutInflater.from(parent.context), parent, false - ), + ) ) } private enum class ItemPosition { - SINGLE, FIRST, MIDDLE, LAST; + SINGLE, + FIRST, + MIDDLE, + LAST; fun backgroundRes(): Int = when (this) { SINGLE -> R.drawable.rounded_corners_listitem_single_background @@ -110,11 +117,7 @@ class UserInfoAdapter(val context: Context, val displayList: Map 0 } - override fun onBindHeaderViewHolder( - holder: SectionedViewHolder?, - section: Int, - expanded: Boolean - ) { + override fun onBindHeaderViewHolder(holder: SectionedViewHolder?, section: Int, expanded: Boolean) { val title = when (section) { SECTION_GROUPS -> context.getString(R.string.user_info_groups) SECTION_USERINFO -> context.getString(R.string.user_info_profile) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt index 7b276ed708ec..dcfe1e6b7583 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/notifications/NotificationsFragment.kt @@ -25,7 +25,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -67,7 +66,6 @@ class NotificationsFragment : private var binding: NotificationsLayoutBinding? = null private var adapter: NotificationListAdapter? = null - private var snackbar: Snackbar? = null private var optionalUser: Optional? = null @Inject @@ -245,11 +243,6 @@ class NotificationsFragment : private fun setupPushWarning() { if (!resources.getBoolean(R.bool.show_push_warning)) return - if (snackbar?.isShown == false) { - snackbar?.show() - return - } - val pushUrl = resources.getString(R.string.push_server_url) if (pushUrl.isEmpty() && BuildHelper.isFlavourGPlay()) return @@ -260,9 +253,7 @@ class NotificationsFragment : else -> return } - snackbar = binding?.emptyList?.emptyListView?.let { - Snackbar.make(it, messageRes, Snackbar.LENGTH_INDEFINITE).also { s -> s.show() } - } + DisplayUtils.showSnackMessage(this, messageRes) } private fun isUsingOldLogin(): Boolean { diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt index 4fafe5821af0..6e0f7f017bcf 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt @@ -29,9 +29,14 @@ class NavigatorActivity : DrawerActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityNavigatorBinding.inflate(layoutInflater) - setContentView(R.layout.activity_navigator) + setContentView(binding.root) + + val screen = requireNotNull( + intent.getParcelableArgument(EXTRA_SCREEN, NavigatorScreen::class.java) + ) { + "NavigatorScreen is required in intent extras" + } - val screen = intent.getParcelableArgument(EXTRA_SCREEN, NavigatorScreen::class.java) ?: return navigator = Navigator(supportFragmentManager, binding.fragmentContainerView) setupBackPressedHandler() pushOrRestoreScreen(savedInstanceState, screen) diff --git a/app/src/main/res/layout/notifications_layout.xml b/app/src/main/res/layout/notifications_layout.xml index 544180b8dd68..072171a50ff7 100644 --- a/app/src/main/res/layout/notifications_layout.xml +++ b/app/src/main/res/layout/notifications_layout.xml @@ -2,18 +2,16 @@ + android:layout_height="match_parent"> Date: Mon, 13 Apr 2026 09:56:50 +0200 Subject: [PATCH 18/18] simplify logic Signed-off-by: alperozturk96 --- .../owncloud/android/ui/adapter/NotificationListAdapter.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt index c5249217b7a2..00677da4fa82 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.kt @@ -57,9 +57,12 @@ class NotificationListAdapter( private val notificationsList = ArrayList() // region Adapter overrides - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NotificationViewHolder( - NotificationListItemBinding.inflate(LayoutInflater.from(fragment.requireContext())) + NotificationListItemBinding.inflate( + LayoutInflater.from(fragment.requireContext()), + parent, + false + ) ) override fun getItemCount() = notificationsList.size