From f171b0bae465659d9a7b7ca86efff5f19ad1de91 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 11:42:27 +0100 Subject: [PATCH 1/7] fix(crash): display utils show snackbar Signed-off-by: alperozturk96 --- .../fragment/FileDetailSharingFragment.java | 34 ++++++------------- .../FileDetailsSharingProcessFragment.kt | 12 +++---- .../contactsbackup/BackupListFragment.java | 7 +--- .../ui/preview/PreviewTextFileFragment.java | 2 +- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index c33cb35aa0de..48a1ac642937 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -186,10 +186,7 @@ private void fetchSharees() { return Unit.INSTANCE; }, () -> { showShareContainer(); - final var view = getView(); - if (view != null) { - DisplayUtils.showSnackMessage(view, R.string.error_fetching_sharees); - } + DisplayUtils.showSnackMessage(this, R.string.error_fetching_sharees); return Unit.INSTANCE; }); } @@ -414,10 +411,7 @@ public void copyInternalLink() { OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); if (account == null) { - final var view = getView(); - if (view != null) { - DisplayUtils.showSnackMessage(view, getString(R.string.could_not_retrieve_url)); - } + DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_url); return; } @@ -581,10 +575,7 @@ public void refreshSharesFromDB() { } if (internalShareeListAdapter == null) { - final var view = getView(); - if (view != null) { - DisplayUtils.showSnackMessage(view, getString(R.string.could_not_retrieve_shares)); - } + DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_shares); return; } @@ -642,7 +633,7 @@ private void pickContactEmail() { if (intent.resolveActivity(requireContext().getPackageManager()) != null) { onContactSelectionResultLauncher.launch(intent); } else { - DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.file_detail_sharing_fragment_no_contact_app_message)); + DisplayUtils.showSnackMessage(this, R.string.file_detail_sharing_fragment_no_contact_app_message); } } @@ -665,16 +656,16 @@ private void handleContactResult(@NonNull Uri contactUri) { binding.searchView.requestFocus(); }); } else { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed); + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address."); } } else { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed); + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found."); } cursor.close(); } else { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed); + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null."); } } @@ -737,10 +728,7 @@ public void unShare(OCShare share) { fileDataStorageManager.updateFileEntity(entity); } } else { - final var view = getView(); - if (view != null) { - DisplayUtils.showSnackMessage(view, getString(R.string.failed_update_ui)); - } + DisplayUtils.showSnackMessage(this, R.string.failed_update_ui); } } @@ -778,7 +766,7 @@ public void openShareDetailWithCustomPermissions(OCShare share) { if (isGranted) { pickContactEmail(); } else { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.contact_no_permission); + DisplayUtils.showSnackMessage(this, R.string.contact_no_permission); } }); @@ -789,13 +777,13 @@ public void openShareDetailWithCustomPermissions(OCShare share) { if (result.getResultCode() == Activity.RESULT_OK) { Intent intent = result.getData(); if (intent == null) { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed); + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); return; } Uri contactUri = intent.getData(); if (contactUri == null) { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed); + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); return; } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt index d97dfc911c40..a8270ade7b05 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt @@ -752,14 +752,14 @@ class FileDetailsSharingProcessFragment : @Suppress("ReturnCount") private fun validateShareProcessFirst() { if (permission == OCShare.NO_PERMISSION) { - DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected) + DisplayUtils.showSnackMessage(this, R.string.no_share_permission_selected) return } if (binding.shareProcessSetPasswordSwitch.isChecked && binding.shareProcessEnterPassword.text?.isBlank() == true ) { - DisplayUtils.showSnackMessage(binding.root, R.string.share_link_empty_password) + DisplayUtils.showSnackMessage(this, R.string.share_link_empty_password) return } @@ -773,7 +773,7 @@ class FileDetailsSharingProcessFragment : if (binding.shareProcessChangeNameSwitch.isChecked && binding.shareProcessChangeName.text?.isBlank() == true ) { - DisplayUtils.showSnackMessage(binding.root, R.string.label_empty) + DisplayUtils.showSnackMessage(this, R.string.label_empty) return } @@ -790,13 +790,13 @@ class FileDetailsSharingProcessFragment : @Suppress("ReturnCount") private fun createShareOrUpdateNoteShare() { if (!isAnySharePermissionChecked()) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.share_option_required) + DisplayUtils.showSnackMessage(this, R.string.share_option_required) return } val noteText = binding.noteText.text.toString().trim() if (file == null && (share != null && share?.note == noteText)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.share_cannot_update_empty_note) + DisplayUtils.showSnackMessage(this, R.string.share_cannot_update_empty_note) return } @@ -807,7 +807,7 @@ class FileDetailsSharingProcessFragment : } file == null -> { - DisplayUtils.showSnackMessage(requireActivity(), R.string.file_not_found_cannot_share) + DisplayUtils.showSnackMessage(this, R.string.file_not_found_cannot_share) return } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java index 41ff60d0652e..20148300aa88 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java @@ -462,12 +462,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } private void showPermissionErrorMessage() { - final var view = getView(); - if (view != null) { - DisplayUtils.showSnackMessage(view, R.string.contactlist_no_permission); - } else { - DisplayUtils.showSnackMessage(this, R.string.contactlist_no_permission); - } + DisplayUtils.showSnackMessage(this, R.string.contactlist_no_permission); } private Unit onDownloadUpdate(Transfer download) { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index 9be03392fcbc..09ac5d38b681 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -289,7 +289,7 @@ private void showFileActions(OCFile file) { private void onFileActionChosen(final int itemId) { if (itemId == R.id.action_send_share_file) { if (getFile().isSharedWithMe() && !getFile().canReshare()) { - DisplayUtils.showSnackMessage(getView(), R.string.resharing_is_not_allowed); + DisplayUtils.showSnackMessage(this, R.string.resharing_is_not_allowed); } else { containerActivity.getFileOperationsHelper().sendShareFile(getFile()); } From 6097e6c17f810a159088ad2149f837ad16b54f97 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:05:35 +0100 Subject: [PATCH 2/7] simplify snackbar logic, run on ui thread always Signed-off-by: alperozturk96 --- .../nextcloud/client/ActivitiesActivityIT.kt | 8 -- .../utils/DisplayUtilsFragmentSnackbarTest.kt | 54 +++++++ .../ui/activities/ActivitiesActivity.java | 16 +-- .../owncloud/android/utils/DisplayUtils.java | 133 ++++++------------ 4 files changed, 103 insertions(+), 108 deletions(-) create mode 100644 app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index e6b3bef08b7d..3e403d2203c5 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt @@ -34,10 +34,6 @@ class ActivitiesActivityIT : AbstractIT() { @ScreenshotTest fun openDrawer() { launchActivity().use { scenario -> - scenario.onActivity { sut -> - sut.dismissSnackbar() - } - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) scenario.onActivity { sut -> @@ -54,7 +50,6 @@ class ActivitiesActivityIT : AbstractIT() { fun loading() { launchActivity().use { scenario -> scenario.onActivity { sut -> - sut.dismissSnackbar() sut.binding.emptyList.root.visibility = View.GONE sut.binding.swipeContainingList.visibility = View.GONE sut.binding.loadingContent.visibility = View.VISIBLE @@ -76,7 +71,6 @@ class ActivitiesActivityIT : AbstractIT() { scenario.onActivity { sut -> sut.showActivities(mutableListOf(), nextcloudClient, -1) sut.setProgressIndicatorState(false) - sut.dismissSnackbar() } val screenShotName = createName(testClassName + "_" + "empty", "") @@ -170,7 +164,6 @@ class ActivitiesActivityIT : AbstractIT() { scenario.onActivity { sut -> sut.showActivities(activities as List?, nextcloudClient, -1) sut.setProgressIndicatorState(false) - sut.dismissSnackbar() } val screenShotName = createName(testClassName + "_" + "showActivities", "") @@ -189,7 +182,6 @@ class ActivitiesActivityIT : AbstractIT() { scenario.onActivity { sut -> sut.showEmptyContent("Error", "Error! Please try again later!") sut.setProgressIndicatorState(false) - sut.dismissSnackbar() } val screenShotName = createName(testClassName + "_" + "error", "") diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt new file mode 100644 index 000000000000..0a1805c325cc --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.utils + +import androidx.annotation.StringRes +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +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.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.nextcloud.test.TestActivity +import com.owncloud.android.R +import org.junit.Test + +class DisplayUtilsFragmentSnackbarTest { + + class NormalTestFragment : Fragment() + class DialogTestFragment : DialogFragment() + class BottomSheetTestFragment : BottomSheetDialogFragment() + + private fun testFragmentSnackbar(fragment: Fragment, @StringRes msg: Int) { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + sut.addFragment(fragment) + DisplayUtils.showSnackMessage(fragment, msg) + } + + onView(isRoot()).check(matches(isDisplayed())) + } + } + + @Test + fun testNormalFragmentSnackbar() { + testFragmentSnackbar(NormalTestFragment(), R.string.app_name) + } + + @Test + fun testDialogFragmentSnackbar() { + testFragmentSnackbar(DialogTestFragment(), R.string.app_name) + } + + @Test + fun testBottomSheetFragmentSnackbar() { + testFragmentSnackbar(BottomSheetTestFragment(), R.string.app_name) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java index 2e3a13a6dabd..5327d157a9f7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java @@ -11,7 +11,6 @@ import android.view.MenuItem; import android.view.View; -import com.google.android.material.snackbar.Snackbar; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.common.NextcloudClient; @@ -54,7 +53,6 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn private long lastGiven; private boolean isLoadingActivities; private ActivitiesContract.ActionListener actionListener; - private Snackbar snackbar; @Inject ActivitiesRepository activitiesRepository; @Inject FilesRepository filesRepository; @@ -191,7 +189,7 @@ public void showActivities(List activities, NextcloudClient client, long public void showActivitiesLoadError(String error) { connectivityService.isNetworkAndServerAvailable(result -> { if (result) { - snackbar = DisplayUtils.showSnackMessage(this, error); + DisplayUtils.showSnackMessage(this, error); } else { showEmptyContent(getString(R.string.server_not_reachable), getString(R.string.server_not_reachable_content)); @@ -217,12 +215,12 @@ public void showActivityDetailUI(OCFile ocFile) { @Override public void showActivityDetailUIIsNull() { - snackbar = DisplayUtils.showSnackMessage(this, R.string.file_not_found); + DisplayUtils.showSnackMessage(this, R.string.file_not_found); } @Override public void showActivityDetailError(String error) { - snackbar = DisplayUtils.showSnackMessage(this, error); + DisplayUtils.showSnackMessage(this, error); } @Override @@ -255,12 +253,4 @@ protected void onStop() { actionListener.onStop(); } - - @VisibleForTesting - public void dismissSnackbar() { - if (snackbar != null && snackbar.isShown()) { - snackbar.dismiss(); - snackbar = null; - } - } } diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 45a2a2831ee8..39aaed2b5597 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -35,6 +35,8 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -124,6 +126,7 @@ public final class DisplayUtils { public static final String MONTH_YEAR_PATTERN = "MMMM yyyy"; public static final String MONTH_PATTERN = "MMMM"; public static final String YEAR_PATTERN = "yyyy"; + private static final Handler mainLooper = new Handler(Looper.getMainLooper()); public static final int SVG_SIZE = 512; private static Map mimeType2HumanReadable; @@ -574,83 +577,69 @@ public static String getData(InputStream inputStream) { return text.toString(); } - public static Snackbar showSnackMessage(Fragment fragment, @StringRes int messageResource) { + // region snackbar + public static void showSnackMessage(Fragment fragment, @StringRes int messageResource) { if (fragment == null) { - return null; + return; } final var activity = fragment.getActivity(); if (activity == null) { - return null; + return; } - return showSnackMessage(activity, messageResource); + showSnackMessage(activity, messageResource); } - /** - * Show a temporary message in a {@link Snackbar} bound to the content view. - * - * @param activity The {@link Activity} to which's content view the {@link Snackbar} is bound. - * @param messageResource The resource id of the string resource to use. Can be formatted text. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(Activity activity, @StringRes int messageResource) { - return showSnackMessage(activity.findViewById(android.R.id.content), messageResource); + public static void showSnackMessage(Activity activity, @StringRes int messageResource) { + showSnackMessage(activity.findViewById(android.R.id.content), messageResource); } - /** - * Show a temporary message in a {@link Snackbar} bound to the content view. - * - * @param activity The {@link Activity} to which's content view the {@link Snackbar} is bound. - * @param message Message to show. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(Activity activity, String message) { - final Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG); - var fab = findFABView(activity); - if (fab != null && fab.getVisibility() == View.VISIBLE) { - snackbar.setAnchorView(fab); - } - snackbar.show(); - return snackbar; + public static void showSnackMessage(Activity activity, @StringRes int messageResource, Object... formatArgs) { + showSnackMessage(activity, activity.findViewById(android.R.id.content), messageResource, formatArgs); } - private static View findFABView(Activity activity) { - return activity.findViewById(R.id.fab_main); + public static void showSnackMessage(Context context, View view, @StringRes int messageResource, Object... formatArgs) { + final Snackbar snackbar = Snackbar.make(view, String.format(context.getString(messageResource, formatArgs)), Snackbar.LENGTH_LONG); + snackbar.show(); } - private static View findFABView(View view) { - return view.findViewById(R.id.fab_main); + public static void showSnackMessage(Activity activity, String message) { + activity.runOnUiThread(() -> { + final Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG); + var fab = findFABView(activity); + if (fab != null && fab.getVisibility() == View.VISIBLE) { + snackbar.setAnchorView(fab); + } + snackbar.show(); + }); } - /** - * Show a temporary message in a {@link Snackbar} bound to the given view. - * - * @param view The view the {@link Snackbar} is bound to. - * @param messageResource The resource id of the string resource to use. Can be formatted text. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(View view, @StringRes int messageResource) { + public static void showSnackMessage(View view, @StringRes int messageResource) { final Snackbar snackbar = Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG); - var fab = findFABView(view.getRootView()); - if (fab != null && fab.getVisibility() == View.VISIBLE) { - snackbar.setAnchorView(fab); - } - snackbar.show(); - return snackbar; + mainLooper.post(() -> { + var fab = findFABView(view.getRootView()); + if (fab != null && fab.getVisibility() == View.VISIBLE) { + snackbar.setAnchorView(fab); + } + snackbar.show(); + }); } - /** - * Show a temporary message in a {@link Snackbar} bound to the given view. - * - * @param view The view the {@link Snackbar} is bound to. - * @param message The message. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(View view, String message) { - final Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); - snackbar.show(); - return snackbar; + public static void showSnackMessage(View view, String message) { + mainLooper.post(() -> { + final Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); + snackbar.show(); + }); + } + // endregion + + private static View findFABView(Activity activity) { + return activity.findViewById(R.id.fab_main); + } + + private static View findFABView(View view) { + return view.findViewById(R.id.fab_main); } /** @@ -664,36 +653,6 @@ public static Snackbar createSnackbar(View view, @StringRes int messageResource, return Snackbar.make(view, messageResource, length); } - /** - * Show a temporary message in a {@link Snackbar} bound to the content view. - * - * @param activity The {@link Activity} to which's content view the {@link Snackbar} is bound. - * @param messageResource The resource id of the string resource to use. Can be formatted text. - * @param formatArgs The format arguments that will be used for substitution. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(Activity activity, @StringRes int messageResource, Object... formatArgs) { - return showSnackMessage(activity, activity.findViewById(android.R.id.content), messageResource, formatArgs); - } - - /** - * Show a temporary message in a {@link Snackbar} bound to the content view. - * - * @param context to load resources. - * @param view The content view the {@link Snackbar} is bound to. - * @param messageResource The resource id of the string resource to use. Can be formatted text. - * @param formatArgs The format arguments that will be used for substitution. - * @return The created {@link Snackbar} - */ - public static Snackbar showSnackMessage(Context context, View view, @StringRes int messageResource, Object... formatArgs) { - final Snackbar snackbar = Snackbar.make( - view, - String.format(context.getString(messageResource, formatArgs)), - Snackbar.LENGTH_LONG); - snackbar - .show(); - return snackbar; - } // Solution inspired by https://stackoverflow.com/questions/34936590/why-isnt-my-vector-drawable-scaling-as-expected // Copied from https://raw.githubusercontent.com/nextcloud/talk-android/8ec8606bc61878e87e3ac8ad32c8b72d4680013c/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java From 551c1705a3dd5e8f375226561d7cb3a54924c577 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:24:17 +0100 Subject: [PATCH 3/7] add tests Signed-off-by: alperozturk96 --- .../utils/DisplayUtilsFragmentSnackbarTest.kt | 54 ------ .../android/utils/DisplayUtilsSnackbarTest.kt | 163 ++++++++++++++++++ 2 files changed, 163 insertions(+), 54 deletions(-) delete mode 100644 app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt create mode 100644 app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt deleted file mode 100644 index 0a1805c325cc..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsFragmentSnackbarTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.owncloud.android.utils - -import androidx.annotation.StringRes -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment -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.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.nextcloud.test.TestActivity -import com.owncloud.android.R -import org.junit.Test - -class DisplayUtilsFragmentSnackbarTest { - - class NormalTestFragment : Fragment() - class DialogTestFragment : DialogFragment() - class BottomSheetTestFragment : BottomSheetDialogFragment() - - private fun testFragmentSnackbar(fragment: Fragment, @StringRes msg: Int) { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - sut.addFragment(fragment) - DisplayUtils.showSnackMessage(fragment, msg) - } - - onView(isRoot()).check(matches(isDisplayed())) - } - } - - @Test - fun testNormalFragmentSnackbar() { - testFragmentSnackbar(NormalTestFragment(), R.string.app_name) - } - - @Test - fun testDialogFragmentSnackbar() { - testFragmentSnackbar(DialogTestFragment(), R.string.app_name) - } - - @Test - fun testBottomSheetFragmentSnackbar() { - testFragmentSnackbar(BottomSheetTestFragment(), R.string.app_name) - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt new file mode 100644 index 000000000000..49541d942854 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt @@ -0,0 +1,163 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.utils + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import androidx.annotation.StringRes +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.nextcloud.client.onboarding.FirstRunActivity +import com.nextcloud.test.TestActivity +import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity +import org.hamcrest.Matchers.anyOf +import org.junit.After +import org.junit.Before +import org.junit.Test + +class DisplayUtilsSnackbarTest { + + class NormalTestFragment : Fragment() + class DialogTestFragment : DialogFragment() + class BottomSheetTestFragment : BottomSheetDialogFragment() + + @Before + fun setUp() { + Intents.init() + val cancelledResult = Instrumentation.ActivityResult(Activity.RESULT_CANCELED, Intent()) + intending( + anyOf( + hasComponent(AuthenticatorActivity::class.java.name), + hasComponent(FirstRunActivity::class.java.name) + ) + ).respondWith(cancelledResult) + } + + @After + fun tearDown() { + Intents.release() + } + + private fun assertSnackbarVisible(msg: String) { + onView(withText(msg)).check(matches(isDisplayed())) + } + + private fun assertSnackbarVisible(@StringRes msgRes: Int) { + onView(withText(msgRes)).check(matches(isDisplayed())) + } + + private fun testFragmentSnackbar(fragment: Fragment, @StringRes msgRes: Int) { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + sut.addFragment(fragment) + } + scenario.onActivity { + DisplayUtils.showSnackMessage(fragment, msgRes) + } + assertSnackbarVisible(msgRes) + } + } + + @Test + fun testNormalFragmentSnackbar() { + testFragmentSnackbar(NormalTestFragment(), R.string.app_name) + } + + @Test + fun testDialogFragmentSnackbar() { + testFragmentSnackbar(DialogTestFragment(), R.string.app_name) + } + + @Test + fun testBottomSheetFragmentSnackbar() { + testFragmentSnackbar(BottomSheetTestFragment(), R.string.app_name) + } + + @Test + fun testNullFragmentSnackbar_doesNotCrash() { + DisplayUtils.showSnackMessage(null as Fragment?, R.string.app_name) + } + + @Test + fun testActivityStringResSnackbar() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + DisplayUtils.showSnackMessage(sut, R.string.app_name) + } + assertSnackbarVisible(R.string.app_name) + } + } + + @Test + fun testActivityStringSnackbar() { + launchActivity().use { scenario -> + var message = "" + scenario.onActivity { sut -> + message = sut.getString(R.string.app_name) + DisplayUtils.showSnackMessage(sut, message) + } + assertSnackbarVisible(message) + } + } + + @Test + fun testViewStringResSnackbar() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val contentView = sut.findViewById(android.R.id.content) + DisplayUtils.showSnackMessage(contentView, R.string.app_name) + } + assertSnackbarVisible(R.string.app_name) + } + } + + @Test + fun testViewStringSnackbar() { + launchActivity().use { scenario -> + var message = "" + scenario.onActivity { sut -> + message = sut.getString(R.string.app_name) + val contentView = sut.findViewById(android.R.id.content) + DisplayUtils.showSnackMessage(contentView, message) + } + assertSnackbarVisible(message) + } + } + + @Test + fun testActivityStringResWithFormatArgsSnackbar() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + DisplayUtils.showSnackMessage(sut, R.string.app_name) + } + assertSnackbarVisible(R.string.app_name) + } + } + + @Test + fun testContextViewStringResWithFormatArgsSnackbar() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val contentView = sut.findViewById(android.R.id.content) + DisplayUtils.showSnackMessage(sut, contentView, R.string.app_name) + } + assertSnackbarVisible(R.string.app_name) + } + } +} From bf8d9c6b06be39244c9c71cee71a9f8fc774deb6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:28:07 +0100 Subject: [PATCH 4/7] add tests Signed-off-by: alperozturk96 --- .../utils/{DisplayUtilsSnackbarTest.kt => SnackbarTests.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/androidTest/java/com/owncloud/android/utils/{DisplayUtilsSnackbarTest.kt => SnackbarTests.kt} (99%) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt similarity index 99% rename from app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt rename to app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt index 49541d942854..464301f3c836 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsSnackbarTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt @@ -31,7 +31,7 @@ import org.junit.After import org.junit.Before import org.junit.Test -class DisplayUtilsSnackbarTest { +class SnackbarTests { class NormalTestFragment : Fragment() class DialogTestFragment : DialogFragment() From eaec97af1b8e1d109880843b4cd54730bf206c9b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:33:36 +0100 Subject: [PATCH 5/7] add tests Signed-off-by: alperozturk96 --- .../owncloud/android/utils/SnackbarTests.kt | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt b/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt index 464301f3c836..90980f2e5350 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/SnackbarTests.kt @@ -90,7 +90,7 @@ class SnackbarTests { } @Test - fun testNullFragmentSnackbar_doesNotCrash() { + fun testNullFragmentSnackbarShouldNotCrash() { DisplayUtils.showSnackMessage(null as Fragment?, R.string.app_name) } @@ -139,25 +139,4 @@ class SnackbarTests { assertSnackbarVisible(message) } } - - @Test - fun testActivityStringResWithFormatArgsSnackbar() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - DisplayUtils.showSnackMessage(sut, R.string.app_name) - } - assertSnackbarVisible(R.string.app_name) - } - } - - @Test - fun testContextViewStringResWithFormatArgsSnackbar() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - val contentView = sut.findViewById(android.R.id.content) - DisplayUtils.showSnackMessage(sut, contentView, R.string.app_name) - } - assertSnackbarVisible(R.string.app_name) - } - } } From 5c2c11c862755b0b5168e4059ef0b4ecc7eb9b0f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:35:40 +0100 Subject: [PATCH 6/7] call inside main looper Signed-off-by: alperozturk96 --- .../main/java/com/owncloud/android/utils/DisplayUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 39aaed2b5597..7f3ea774228d 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -600,13 +600,13 @@ public static void showSnackMessage(Activity activity, @StringRes int messageRes } public static void showSnackMessage(Context context, View view, @StringRes int messageResource, Object... formatArgs) { - final Snackbar snackbar = Snackbar.make(view, String.format(context.getString(messageResource, formatArgs)), Snackbar.LENGTH_LONG); + final var snackbar = Snackbar.make(view, String.format(context.getString(messageResource, formatArgs)), Snackbar.LENGTH_LONG); snackbar.show(); } public static void showSnackMessage(Activity activity, String message) { activity.runOnUiThread(() -> { - final Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG); + final var snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG); var fab = findFABView(activity); if (fab != null && fab.getVisibility() == View.VISIBLE) { snackbar.setAnchorView(fab); @@ -616,8 +616,8 @@ public static void showSnackMessage(Activity activity, String message) { } public static void showSnackMessage(View view, @StringRes int messageResource) { - final Snackbar snackbar = Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG); mainLooper.post(() -> { + final var snackbar = Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG); var fab = findFABView(view.getRootView()); if (fab != null && fab.getVisibility() == View.VISIBLE) { snackbar.setAnchorView(fab); From be71a1f2046d617f940b19957a18af947c003bfe Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Feb 2026 12:41:24 +0100 Subject: [PATCH 7/7] add null checks Signed-off-by: alperozturk96 --- .../owncloud/android/utils/DisplayUtils.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 7f3ea774228d..463532fd28b7 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -580,11 +580,13 @@ public static String getData(InputStream inputStream) { // region snackbar public static void showSnackMessage(Fragment fragment, @StringRes int messageResource) { if (fragment == null) { + Log_OC.e(TAG, "snackbar cannot be shown fragment is null"); return; } final var activity = fragment.getActivity(); if (activity == null) { + Log_OC.e(TAG, "snackbar cannot be shown activity is null"); return; } @@ -592,19 +594,39 @@ public static void showSnackMessage(Fragment fragment, @StringRes int messageRes } public static void showSnackMessage(Activity activity, @StringRes int messageResource) { + if (activity == null) { + Log_OC.e(TAG, "snackbar cannot be shown activity is null"); + return; + } + showSnackMessage(activity.findViewById(android.R.id.content), messageResource); } public static void showSnackMessage(Activity activity, @StringRes int messageResource, Object... formatArgs) { + if (activity == null) { + Log_OC.e(TAG, "snackbar cannot be shown activity is null"); + return; + } + showSnackMessage(activity, activity.findViewById(android.R.id.content), messageResource, formatArgs); } public static void showSnackMessage(Context context, View view, @StringRes int messageResource, Object... formatArgs) { + if (context == null || view == null) { + Log_OC.e(TAG, "snackbar cannot be shown view is null"); + return; + } + final var snackbar = Snackbar.make(view, String.format(context.getString(messageResource, formatArgs)), Snackbar.LENGTH_LONG); snackbar.show(); } public static void showSnackMessage(Activity activity, String message) { + if (activity == null) { + Log_OC.e(TAG, "snackbar cannot be shown activity is null"); + return; + } + activity.runOnUiThread(() -> { final var snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG); var fab = findFABView(activity); @@ -616,6 +638,11 @@ public static void showSnackMessage(Activity activity, String message) { } public static void showSnackMessage(View view, @StringRes int messageResource) { + if (view == null) { + Log_OC.e(TAG, "snackbar cannot be shown view is null"); + return; + } + mainLooper.post(() -> { final var snackbar = Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG); var fab = findFABView(view.getRootView()); @@ -627,6 +654,11 @@ public static void showSnackMessage(View view, @StringRes int messageResource) { } public static void showSnackMessage(View view, String message) { + if (view == null) { + Log_OC.e(TAG, "snackbar cannot be shown view is null"); + return; + } + mainLooper.post(() -> { final Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); snackbar.show();