Skip to content

Commit 30dd5aa

Browse files
committed
Login related customization.
NMC-3250 -> opening login flow in internal webview instead of external browser. NMC-1885: Splash screen customized NMC-571 -- removed usesCleartextTraffic from Manifest. NMC-3773 -- close app on access denied error comes NMC-3964 -- fix app crash on logout for Xiaomi Android 15 devices NMC-3936 & NMC-3813 -- Enabled edge-to-edge for Api level 35. NMC-4351 -- fix redirection to login screen on timeout NMC-4743 -- fix logout crash from trashbin tab
1 parent 5289f60 commit 30dd5aa

File tree

14 files changed

+346
-310
lines changed

14 files changed

+346
-310
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 TSI-mc <surinder.kumar@t-systems.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nmc.android
9+
10+
import android.content.Context
11+
import android.content.res.Configuration
12+
import android.util.DisplayMetrics
13+
import androidx.test.core.app.ApplicationProvider
14+
import androidx.test.ext.junit.runners.AndroidJUnit4
15+
import com.owncloud.android.R
16+
import junit.framework.TestCase.assertEquals
17+
import junit.framework.TestCase.assertTrue
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import java.util.Locale
21+
22+
/**
23+
* Test class to verify the strings, dimens and bool customized in this branch PR for NMC
24+
*/
25+
@RunWith(AndroidJUnit4::class)
26+
class LoginResourceTest {
27+
28+
private val baseContext = ApplicationProvider.getApplicationContext<Context>()
29+
30+
private val localizedStringMap = mapOf(
31+
R.string.splashScreenBold to ExpectedLocalizedString(
32+
translations = mapOf(
33+
Locale.ENGLISH to "Magenta",
34+
)
35+
),
36+
R.string.splashScreenNormal to ExpectedLocalizedString(
37+
translations = mapOf(
38+
Locale.ENGLISH to "CLOUD",
39+
)
40+
),
41+
)
42+
43+
@Test
44+
fun verifyLocalizedStrings() {
45+
localizedStringMap.forEach { (stringRes, expected) ->
46+
expected.translations.forEach { (locale, expectedText) ->
47+
48+
val config = Configuration(baseContext.resources.configuration)
49+
config.setLocale(locale)
50+
51+
val localizedContext = baseContext.createConfigurationContext(config)
52+
val actualText = localizedContext.getString(stringRes)
53+
54+
assertEquals(
55+
"Mismatch for ${baseContext.resources.getResourceEntryName(stringRes)} in $locale",
56+
expectedText,
57+
actualText
58+
)
59+
}
60+
}
61+
}
62+
63+
data class ExpectedLocalizedString(val translations: Map<Locale, String>)
64+
65+
private val expectedDimenMap = mapOf(
66+
R.dimen.splash_image_size to ExpectedDimen(
67+
default = 116f,
68+
unit = DimenUnit.DP
69+
),
70+
)
71+
72+
@Test
73+
fun validateDefaultDimens() {
74+
validateDimens(
75+
configModifier = { it }, // no change → default values
76+
) { it.default to it.unit }
77+
}
78+
79+
@Test
80+
fun validate_sw600dp_Dimens() {
81+
validateDimens(configModifier = { config ->
82+
config.smallestScreenWidthDp = 600
83+
config
84+
}) { it.alt to it.unit }
85+
}
86+
87+
private fun validateDimens(
88+
configModifier: (Configuration) -> Configuration,
89+
selector: (ExpectedDimen) -> Pair<Float?, DimenUnit>
90+
) {
91+
val baseConfig = Configuration(baseContext.resources.configuration)
92+
val testConfig = configModifier(baseConfig)
93+
val testContext = baseContext.createConfigurationContext(testConfig)
94+
val dm = testContext.resources.displayMetrics
95+
val config = testContext.resources.configuration
96+
expectedDimenMap.forEach { (resId, entry) ->
97+
val (value, unit) = selector(entry)
98+
val actualPx = testContext.resources.getDimension(resId)
99+
value?.let {
100+
val expectedPx = convertToPx(value, unit, dm, config)
101+
assertEquals(
102+
"Mismatch for ${testContext.resources.getResourceEntryName(resId)} ($unit)",
103+
expectedPx,
104+
actualPx,
105+
0.01f
106+
)
107+
}
108+
}
109+
}
110+
111+
private fun convertToPx(
112+
value: Float,
113+
unit: DimenUnit,
114+
dm: DisplayMetrics,
115+
config: Configuration
116+
): Float {
117+
return when (unit) {
118+
DimenUnit.DP -> value * dm.density
119+
DimenUnit.SP -> value * dm.density * config.fontScale
120+
DimenUnit.PX -> value
121+
}
122+
}
123+
124+
data class ExpectedDimen(
125+
val default: Float,
126+
val alt: Float? = null,
127+
val unit: DimenUnit,
128+
)
129+
130+
enum class DimenUnit { DP, SP, PX }
131+
132+
@Test
133+
fun assertMultipleAccountSupportBooleanFalse() {
134+
val actualValue = baseContext.resources.getBoolean(R.bool.multiaccount_support)
135+
assertTrue(!actualValue)
136+
}
137+
}

app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,33 @@
66
*/
77
package com.nmc.android.ui
88

9-
import androidx.test.core.app.launchActivity
109
import androidx.test.espresso.Espresso.onView
1110
import androidx.test.espresso.assertion.ViewAssertions.matches
12-
import androidx.test.espresso.matcher.ViewMatchers
1311
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
14-
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
1512
import androidx.test.espresso.matcher.ViewMatchers.withId
13+
import androidx.test.espresso.matcher.ViewMatchers.withText
14+
import androidx.test.ext.junit.rules.ActivityScenarioRule
1615
import androidx.test.ext.junit.runners.AndroidJUnit4
1716
import com.owncloud.android.AbstractIT
1817
import com.owncloud.android.R
18+
import org.junit.Rule
1919
import org.junit.Test
2020
import org.junit.runner.RunWith
2121

2222
@RunWith(AndroidJUnit4::class)
2323
class LauncherActivityIT : AbstractIT() {
2424

25-
@Test
26-
fun testSplashScreenWithEmptyTitlesShouldHideTitles() {
27-
launchActivity<LauncherActivity>().use { scenario ->
28-
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
29-
onView(
30-
withId(R.id.splashScreenBold)
31-
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
32-
onView(
33-
withId(R.id.splashScreenNormal)
34-
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
35-
}
36-
}
25+
@get:Rule
26+
val activityRule = ActivityScenarioRule(LauncherActivity::class.java)
3727

3828
@Test
39-
fun testSplashScreenWithTitlesShouldShowTitles() {
40-
launchActivity<LauncherActivity>().use { scenario ->
41-
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
42-
43-
scenario.onActivity {
44-
it.setSplashTitles("Example", "Cloud")
45-
}
29+
fun verifyUIElements() {
30+
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
31+
onView(withId(R.id.splashScreenBold)).check(matches(isCompletelyDisplayed()))
32+
onView(withId(R.id.splashScreenNormal)).check(matches(isCompletelyDisplayed()))
4633

47-
val onePercentArea = ViewMatchers.isDisplayingAtLeast(1)
48-
onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea))
49-
onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea))
50-
}
34+
onView(withId(R.id.splashScreenBold)).check(matches(withText("Magenta")))
35+
onView(withId(R.id.splashScreenNormal)).check(matches(withText("CLOUD")))
36+
shortSleep()
5137
}
5238
}

app/src/debug/res/values/setup.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- webview_login_url should be empty in debug mode to show login url input screen
4+
this will be useful in switching the environments during testing -->
5+
<string name="webview_login_url" translatable="false" />
6+
</resources>

app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@
125125
android:roundIcon="@mipmap/ic_launcher"
126126
android:supportsRtl="true"
127127
android:theme="@style/Theme.ownCloud.Toolbar"
128-
android:usesCleartextTraffic="true"
129128
tools:ignore="UnusedAttribute"
130129
tools:replace="android:allowBackup">
131130

app/src/main/java/com/owncloud/android/MainApp.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import android.os.StrictMode;
3636
import android.text.TextUtils;
3737
import android.view.WindowManager;
38+
import android.webkit.WebView;
3839

3940
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
4041
import com.nextcloud.appReview.InAppReviewHelper;
@@ -369,6 +370,8 @@ public void onCreate() {
369370
networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
370371
registerNetworkChangeReceiver();
371372

373+
configureWebViewForMultiProcess();
374+
372375
if (!MDMConfig.INSTANCE.sendFilesSupport(this)) {
373376
disableDocumentsStorageProvider();
374377
}
@@ -384,6 +387,16 @@ public void disableDocumentsStorageProvider() {
384387
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
385388
}
386389

390+
// NMC-3964 fix
391+
// crash was happening for Xiaomi Android 15 devices
392+
private void configureWebViewForMultiProcess(){
393+
String processName = getProcessName();
394+
if (processName != null && !processName.equals(getPackageName())) {
395+
// this ensures each process uses a unique directory, preventing conflicts.
396+
WebView.setDataDirectorySuffix(processName);
397+
}
398+
}
399+
387400
private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> {
388401
if (event == Lifecycle.Event.ON_START) {
389402
Log_OC.d(TAG, "APP IN FOREGROUND");

app/src/main/java/com/owncloud/android/authentication/AccountAuthenticatorActivity.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88

99
import android.accounts.AccountAuthenticatorResponse;
1010
import android.accounts.AccountManager;
11+
import android.graphics.Color;
12+
import android.os.Build;
1113
import android.os.Bundle;
1214

1315
import com.nextcloud.utils.extensions.IntentExtensionsKt;
16+
import com.nextcloud.android.common.ui.util.extensions.WindowExtensionsKt;
1417

18+
import androidx.activity.EdgeToEdge;
19+
import androidx.activity.SystemBarStyle;
1520
import androidx.appcompat.app.AppCompatActivity;
1621

1722
/*
@@ -46,6 +51,14 @@ public final void setAccountAuthenticatorResult(Bundle result) {
4651
*/
4752
@Override
4853
protected void onCreate(Bundle savedInstanceState) {
54+
// NMC-3936 and NMC-3813 fix
55+
boolean isApiLevel35OrHigher = (Build.VERSION.SDK_INT >= 35);
56+
57+
if (isApiLevel35OrHigher) {
58+
enableEdgeToEdge();
59+
WindowExtensionsKt.addSystemBarPaddings(getWindow());
60+
}
61+
4962
super.onCreate(savedInstanceState);
5063

5164
mAccountAuthenticatorResponse =
@@ -58,6 +71,11 @@ protected void onCreate(Bundle savedInstanceState) {
5871
}
5972
}
6073

74+
private void enableEdgeToEdge() {
75+
final var style = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT);
76+
EdgeToEdge.enable(this, style, style);
77+
}
78+
6179
/**
6280
* Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present.
6381
*/

0 commit comments

Comments
 (0)