Skip to content

Commit a259d67

Browse files
Merge pull request #16597 from nextcloud/fix/show-snack-bar-in-nested-scroll-view
fix(crash): show snack bar in nested scroll view
2 parents fdf0680 + 0c75eea commit a259d67

8 files changed

Lines changed: 241 additions & 144 deletions

File tree

app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ class ActivitiesActivityIT : AbstractIT() {
3434
@ScreenshotTest
3535
fun openDrawer() {
3636
launchActivity<ActivitiesActivity>().use { scenario ->
37-
scenario.onActivity { sut ->
38-
sut.dismissSnackbar()
39-
}
40-
4137
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
4238

4339
scenario.onActivity { sut ->
@@ -54,7 +50,6 @@ class ActivitiesActivityIT : AbstractIT() {
5450
fun loading() {
5551
launchActivity<ActivitiesActivity>().use { scenario ->
5652
scenario.onActivity { sut ->
57-
sut.dismissSnackbar()
5853
sut.binding.emptyList.root.visibility = View.GONE
5954
sut.binding.swipeContainingList.visibility = View.GONE
6055
sut.binding.loadingContent.visibility = View.VISIBLE
@@ -76,7 +71,6 @@ class ActivitiesActivityIT : AbstractIT() {
7671
scenario.onActivity { sut ->
7772
sut.showActivities(mutableListOf(), nextcloudClient, -1)
7873
sut.setProgressIndicatorState(false)
79-
sut.dismissSnackbar()
8074
}
8175

8276
val screenShotName = createName(testClassName + "_" + "empty", "")
@@ -170,7 +164,6 @@ class ActivitiesActivityIT : AbstractIT() {
170164
scenario.onActivity { sut ->
171165
sut.showActivities(activities as List<Any>?, nextcloudClient, -1)
172166
sut.setProgressIndicatorState(false)
173-
sut.dismissSnackbar()
174167
}
175168

176169
val screenShotName = createName(testClassName + "_" + "showActivities", "")
@@ -189,7 +182,6 @@ class ActivitiesActivityIT : AbstractIT() {
189182
scenario.onActivity { sut ->
190183
sut.showEmptyContent("Error", "Error! Please try again later!")
191184
sut.setProgressIndicatorState(false)
192-
sut.dismissSnackbar()
193185
}
194186

195187
val screenShotName = createName(testClassName + "_" + "error", "")
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.utils
9+
10+
import android.app.Activity
11+
import android.app.Instrumentation
12+
import android.content.Intent
13+
import androidx.annotation.StringRes
14+
import androidx.fragment.app.DialogFragment
15+
import androidx.fragment.app.Fragment
16+
import androidx.test.core.app.launchActivity
17+
import androidx.test.espresso.Espresso.onView
18+
import androidx.test.espresso.assertion.ViewAssertions.matches
19+
import androidx.test.espresso.intent.Intents
20+
import androidx.test.espresso.intent.Intents.intending
21+
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
22+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
23+
import androidx.test.espresso.matcher.ViewMatchers.withText
24+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
25+
import com.nextcloud.client.onboarding.FirstRunActivity
26+
import com.nextcloud.test.TestActivity
27+
import com.owncloud.android.R
28+
import com.owncloud.android.authentication.AuthenticatorActivity
29+
import org.hamcrest.Matchers.anyOf
30+
import org.junit.After
31+
import org.junit.Before
32+
import org.junit.Test
33+
34+
class SnackbarTests {
35+
36+
class NormalTestFragment : Fragment()
37+
class DialogTestFragment : DialogFragment()
38+
class BottomSheetTestFragment : BottomSheetDialogFragment()
39+
40+
@Before
41+
fun setUp() {
42+
Intents.init()
43+
val cancelledResult = Instrumentation.ActivityResult(Activity.RESULT_CANCELED, Intent())
44+
intending(
45+
anyOf(
46+
hasComponent(AuthenticatorActivity::class.java.name),
47+
hasComponent(FirstRunActivity::class.java.name)
48+
)
49+
).respondWith(cancelledResult)
50+
}
51+
52+
@After
53+
fun tearDown() {
54+
Intents.release()
55+
}
56+
57+
private fun assertSnackbarVisible(msg: String) {
58+
onView(withText(msg)).check(matches(isDisplayed()))
59+
}
60+
61+
private fun assertSnackbarVisible(@StringRes msgRes: Int) {
62+
onView(withText(msgRes)).check(matches(isDisplayed()))
63+
}
64+
65+
private fun testFragmentSnackbar(fragment: Fragment, @StringRes msgRes: Int) {
66+
launchActivity<TestActivity>().use { scenario ->
67+
scenario.onActivity { sut ->
68+
sut.addFragment(fragment)
69+
}
70+
scenario.onActivity {
71+
DisplayUtils.showSnackMessage(fragment, msgRes)
72+
}
73+
assertSnackbarVisible(msgRes)
74+
}
75+
}
76+
77+
@Test
78+
fun testNormalFragmentSnackbar() {
79+
testFragmentSnackbar(NormalTestFragment(), R.string.app_name)
80+
}
81+
82+
@Test
83+
fun testDialogFragmentSnackbar() {
84+
testFragmentSnackbar(DialogTestFragment(), R.string.app_name)
85+
}
86+
87+
@Test
88+
fun testBottomSheetFragmentSnackbar() {
89+
testFragmentSnackbar(BottomSheetTestFragment(), R.string.app_name)
90+
}
91+
92+
@Test
93+
fun testNullFragmentSnackbarShouldNotCrash() {
94+
DisplayUtils.showSnackMessage(null as Fragment?, R.string.app_name)
95+
}
96+
97+
@Test
98+
fun testActivityStringResSnackbar() {
99+
launchActivity<TestActivity>().use { scenario ->
100+
scenario.onActivity { sut ->
101+
DisplayUtils.showSnackMessage(sut, R.string.app_name)
102+
}
103+
assertSnackbarVisible(R.string.app_name)
104+
}
105+
}
106+
107+
@Test
108+
fun testActivityStringSnackbar() {
109+
launchActivity<TestActivity>().use { scenario ->
110+
var message = ""
111+
scenario.onActivity { sut ->
112+
message = sut.getString(R.string.app_name)
113+
DisplayUtils.showSnackMessage(sut, message)
114+
}
115+
assertSnackbarVisible(message)
116+
}
117+
}
118+
119+
@Test
120+
fun testViewStringResSnackbar() {
121+
launchActivity<TestActivity>().use { scenario ->
122+
scenario.onActivity { sut ->
123+
val contentView = sut.findViewById<android.view.View>(android.R.id.content)
124+
DisplayUtils.showSnackMessage(contentView, R.string.app_name)
125+
}
126+
assertSnackbarVisible(R.string.app_name)
127+
}
128+
}
129+
130+
@Test
131+
fun testViewStringSnackbar() {
132+
launchActivity<TestActivity>().use { scenario ->
133+
var message = ""
134+
scenario.onActivity { sut ->
135+
message = sut.getString(R.string.app_name)
136+
val contentView = sut.findViewById<android.view.View>(android.R.id.content)
137+
DisplayUtils.showSnackMessage(contentView, message)
138+
}
139+
assertSnackbarVisible(message)
140+
}
141+
}
142+
}

app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import android.view.MenuItem;
1212
import android.view.View;
1313

14-
import com.google.android.material.snackbar.Snackbar;
1514
import com.nextcloud.client.network.ClientFactory;
1615
import com.nextcloud.client.network.ConnectivityService;
1716
import com.nextcloud.common.NextcloudClient;
@@ -54,7 +53,6 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn
5453
private long lastGiven;
5554
private boolean isLoadingActivities;
5655
private ActivitiesContract.ActionListener actionListener;
57-
private Snackbar snackbar;
5856

5957
@Inject ActivitiesRepository activitiesRepository;
6058
@Inject FilesRepository filesRepository;
@@ -191,7 +189,7 @@ public void showActivities(List<Object> activities, NextcloudClient client, long
191189
public void showActivitiesLoadError(String error) {
192190
connectivityService.isNetworkAndServerAvailable(result -> {
193191
if (result) {
194-
snackbar = DisplayUtils.showSnackMessage(this, error);
192+
DisplayUtils.showSnackMessage(this, error);
195193
} else {
196194
showEmptyContent(getString(R.string.server_not_reachable),
197195
getString(R.string.server_not_reachable_content));
@@ -217,12 +215,12 @@ public void showActivityDetailUI(OCFile ocFile) {
217215

218216
@Override
219217
public void showActivityDetailUIIsNull() {
220-
snackbar = DisplayUtils.showSnackMessage(this, R.string.file_not_found);
218+
DisplayUtils.showSnackMessage(this, R.string.file_not_found);
221219
}
222220

223221
@Override
224222
public void showActivityDetailError(String error) {
225-
snackbar = DisplayUtils.showSnackMessage(this, error);
223+
DisplayUtils.showSnackMessage(this, error);
226224
}
227225

228226
@Override
@@ -255,12 +253,4 @@ protected void onStop() {
255253

256254
actionListener.onStop();
257255
}
258-
259-
@VisibleForTesting
260-
public void dismissSnackbar() {
261-
if (snackbar != null && snackbar.isShown()) {
262-
snackbar.dismiss();
263-
snackbar = null;
264-
}
265-
}
266256
}

app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,7 @@ private void fetchSharees() {
186186
return Unit.INSTANCE;
187187
}, () -> {
188188
showShareContainer();
189-
final var view = getView();
190-
if (view != null) {
191-
DisplayUtils.showSnackMessage(view, R.string.error_fetching_sharees);
192-
}
189+
DisplayUtils.showSnackMessage(this, R.string.error_fetching_sharees);
193190
return Unit.INSTANCE;
194191
});
195192
}
@@ -414,10 +411,7 @@ public void copyInternalLink() {
414411
OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount();
415412

416413
if (account == null) {
417-
final var view = getView();
418-
if (view != null) {
419-
DisplayUtils.showSnackMessage(view, getString(R.string.could_not_retrieve_url));
420-
}
414+
DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_url);
421415
return;
422416
}
423417

@@ -581,10 +575,7 @@ public void refreshSharesFromDB() {
581575
}
582576

583577
if (internalShareeListAdapter == null) {
584-
final var view = getView();
585-
if (view != null) {
586-
DisplayUtils.showSnackMessage(view, getString(R.string.could_not_retrieve_shares));
587-
}
578+
DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_shares);
588579
return;
589580
}
590581

@@ -642,7 +633,7 @@ private void pickContactEmail() {
642633
if (intent.resolveActivity(requireContext().getPackageManager()) != null) {
643634
onContactSelectionResultLauncher.launch(intent);
644635
} else {
645-
DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.file_detail_sharing_fragment_no_contact_app_message));
636+
DisplayUtils.showSnackMessage(this, R.string.file_detail_sharing_fragment_no_contact_app_message);
646637
}
647638
}
648639

@@ -665,16 +656,16 @@ private void handleContactResult(@NonNull Uri contactUri) {
665656
binding.searchView.requestFocus();
666657
});
667658
} else {
668-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
659+
DisplayUtils.showSnackMessage(this, R.string.email_pick_failed);
669660
Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address.");
670661
}
671662
} else {
672-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
663+
DisplayUtils.showSnackMessage(this, R.string.email_pick_failed);
673664
Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found.");
674665
}
675666
cursor.close();
676667
} else {
677-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
668+
DisplayUtils.showSnackMessage(this, R.string.email_pick_failed);
678669
Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null.");
679670
}
680671
}
@@ -737,10 +728,7 @@ public void unShare(OCShare share) {
737728
fileDataStorageManager.updateFileEntity(entity);
738729
}
739730
} else {
740-
final var view = getView();
741-
if (view != null) {
742-
DisplayUtils.showSnackMessage(view, getString(R.string.failed_update_ui));
743-
}
731+
DisplayUtils.showSnackMessage(this, R.string.failed_update_ui);
744732
}
745733
}
746734

@@ -778,7 +766,7 @@ public void openShareDetailWithCustomPermissions(OCShare share) {
778766
if (isGranted) {
779767
pickContactEmail();
780768
} else {
781-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.contact_no_permission);
769+
DisplayUtils.showSnackMessage(this, R.string.contact_no_permission);
782770
}
783771
});
784772

@@ -789,13 +777,13 @@ public void openShareDetailWithCustomPermissions(OCShare share) {
789777
if (result.getResultCode() == Activity.RESULT_OK) {
790778
Intent intent = result.getData();
791779
if (intent == null) {
792-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
780+
DisplayUtils.showSnackMessage(this, R.string.email_pick_failed);
793781
return;
794782
}
795783

796784
Uri contactUri = intent.getData();
797785
if (contactUri == null) {
798-
DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
786+
DisplayUtils.showSnackMessage(this, R.string.email_pick_failed);
799787
return;
800788
}
801789

app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -752,14 +752,14 @@ class FileDetailsSharingProcessFragment :
752752
@Suppress("ReturnCount")
753753
private fun validateShareProcessFirst() {
754754
if (permission == OCShare.NO_PERMISSION) {
755-
DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected)
755+
DisplayUtils.showSnackMessage(this, R.string.no_share_permission_selected)
756756
return
757757
}
758758

759759
if (binding.shareProcessSetPasswordSwitch.isChecked &&
760760
binding.shareProcessEnterPassword.text?.isBlank() == true
761761
) {
762-
DisplayUtils.showSnackMessage(binding.root, R.string.share_link_empty_password)
762+
DisplayUtils.showSnackMessage(this, R.string.share_link_empty_password)
763763
return
764764
}
765765

@@ -773,7 +773,7 @@ class FileDetailsSharingProcessFragment :
773773
if (binding.shareProcessChangeNameSwitch.isChecked &&
774774
binding.shareProcessChangeName.text?.isBlank() == true
775775
) {
776-
DisplayUtils.showSnackMessage(binding.root, R.string.label_empty)
776+
DisplayUtils.showSnackMessage(this, R.string.label_empty)
777777
return
778778
}
779779

@@ -790,13 +790,13 @@ class FileDetailsSharingProcessFragment :
790790
@Suppress("ReturnCount")
791791
private fun createShareOrUpdateNoteShare() {
792792
if (!isAnySharePermissionChecked()) {
793-
DisplayUtils.showSnackMessage(requireActivity(), R.string.share_option_required)
793+
DisplayUtils.showSnackMessage(this, R.string.share_option_required)
794794
return
795795
}
796796

797797
val noteText = binding.noteText.text.toString().trim()
798798
if (file == null && (share != null && share?.note == noteText)) {
799-
DisplayUtils.showSnackMessage(requireActivity(), R.string.share_cannot_update_empty_note)
799+
DisplayUtils.showSnackMessage(this, R.string.share_cannot_update_empty_note)
800800
return
801801
}
802802

@@ -807,7 +807,7 @@ class FileDetailsSharingProcessFragment :
807807
}
808808

809809
file == null -> {
810-
DisplayUtils.showSnackMessage(requireActivity(), R.string.file_not_found_cannot_share)
810+
DisplayUtils.showSnackMessage(this, R.string.file_not_found_cannot_share)
811811
return
812812
}
813813

0 commit comments

Comments
 (0)