Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions app/src/androidTest/java/com/nmc/android/LoginResourceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nmc.android

import android.content.Context
import android.content.res.Configuration
import android.util.DisplayMetrics
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.owncloud.android.R
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Locale

/**
* Test class to verify the strings, dimens and bool customized in this branch PR for NMC
*/
@RunWith(AndroidJUnit4::class)
class LoginResourceTest {

private val baseContext = ApplicationProvider.getApplicationContext<Context>()

private val localizedStringMap = mapOf(
R.string.splashScreenBold to ExpectedLocalizedString(
translations = mapOf(
Locale.ENGLISH to "Magenta",
)
),
R.string.splashScreenNormal to ExpectedLocalizedString(
translations = mapOf(
Locale.ENGLISH to "CLOUD",
)
),
)

@Test
fun verifyLocalizedStrings() {
localizedStringMap.forEach { (stringRes, expected) ->
expected.translations.forEach { (locale, expectedText) ->

val config = Configuration(baseContext.resources.configuration)
config.setLocale(locale)

val localizedContext = baseContext.createConfigurationContext(config)
val actualText = localizedContext.getString(stringRes)

assertEquals(
"Mismatch for ${baseContext.resources.getResourceEntryName(stringRes)} in $locale",
expectedText,
actualText
)
}
}
}

data class ExpectedLocalizedString(val translations: Map<Locale, String>)

private val expectedDimenMap = mapOf(
R.dimen.splash_image_size to ExpectedDimen(
default = 116f,
unit = DimenUnit.DP
),
)

@Test
fun validateDefaultDimens() {
validateDimens(
configModifier = { it }, // no change → default values
) { it.default to it.unit }
}

@Test
fun validate_sw600dp_Dimens() {
validateDimens(configModifier = { config ->
config.smallestScreenWidthDp = 600
config
}) { it.alt to it.unit }
}

private fun validateDimens(
configModifier: (Configuration) -> Configuration,
selector: (ExpectedDimen) -> Pair<Float?, DimenUnit>
) {
val baseConfig = Configuration(baseContext.resources.configuration)
val testConfig = configModifier(baseConfig)
val testContext = baseContext.createConfigurationContext(testConfig)
val dm = testContext.resources.displayMetrics
val config = testContext.resources.configuration
expectedDimenMap.forEach { (resId, entry) ->
val (value, unit) = selector(entry)
val actualPx = testContext.resources.getDimension(resId)
value?.let {
val expectedPx = convertToPx(value, unit, dm, config)
assertEquals(
"Mismatch for ${testContext.resources.getResourceEntryName(resId)} ($unit)",
expectedPx,
actualPx,
0.01f
)
}
}
}

private fun convertToPx(
value: Float,
unit: DimenUnit,
dm: DisplayMetrics,
config: Configuration
): Float {
return when (unit) {
DimenUnit.DP -> value * dm.density
DimenUnit.SP -> value * dm.density * config.fontScale
DimenUnit.PX -> value
}
}

data class ExpectedDimen(
val default: Float,
val alt: Float? = null,
val unit: DimenUnit,
)

enum class DimenUnit { DP, SP, PX }

@Test
fun assertMultipleAccountSupportBooleanFalse() {
val actualValue = baseContext.resources.getBoolean(R.bool.multiaccount_support)
assertTrue(!actualValue)
}
}
38 changes: 12 additions & 26 deletions app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,33 @@
*/
package com.nmc.android.ui

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
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LauncherActivityIT : AbstractIT() {

@Test
fun testSplashScreenWithEmptyTitlesShouldHideTitles() {
launchActivity<LauncherActivity>().use { scenario ->
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
onView(
withId(R.id.splashScreenBold)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(
withId(R.id.splashScreenNormal)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
}
@get:Rule
val activityRule = ActivityScenarioRule(LauncherActivity::class.java)

@Test
fun testSplashScreenWithTitlesShouldShowTitles() {
launchActivity<LauncherActivity>().use { scenario ->
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))

scenario.onActivity {
it.setSplashTitles("Example", "Cloud")
}
fun verifyUIElements() {
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
onView(withId(R.id.splashScreenBold)).check(matches(isCompletelyDisplayed()))
onView(withId(R.id.splashScreenNormal)).check(matches(isCompletelyDisplayed()))

val onePercentArea = ViewMatchers.isDisplayingAtLeast(1)
onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea))
onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea))
}
onView(withId(R.id.splashScreenBold)).check(matches(withText("Magenta")))
onView(withId(R.id.splashScreenNormal)).check(matches(withText("CLOUD")))
shortSleep()
}
}
6 changes: 6 additions & 0 deletions app/src/debug/res/values/setup.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- webview_login_url should be empty in debug mode to show login url input screen
this will be useful in switching the environments during testing -->
<string name="webview_login_url" translatable="false" />
</resources>
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.ownCloud.Toolbar"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute"
tools:replace="android:allowBackup">

Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/owncloud/android/MainApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import android.os.StrictMode;
import android.text.TextUtils;
import android.view.WindowManager;
import android.webkit.WebView;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.appReview.InAppReviewHelper;
Expand Down Expand Up @@ -368,6 +369,8 @@ public void onCreate() {
networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
registerNetworkChangeReceiver();

configureWebViewForMultiProcess();

if (!MDMConfig.INSTANCE.sendFilesSupport(this)) {
disableDocumentsStorageProvider();
}
Expand All @@ -383,6 +386,18 @@ public void disableDocumentsStorageProvider() {
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}

// NMC-3964 fix
// crash was happening for Xiaomi Android 15 devices
private void configureWebViewForMultiProcess(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
String processName = getProcessName();
if (processName != null && !processName.equals(getPackageName())) {
// this ensures each process uses a unique directory, preventing conflicts.
WebView.setDataDirectorySuffix(processName);
}
}
}

private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> {
if (event == Lifecycle.Event.ON_START) {
Log_OC.d(TAG, "APP IN FOREGROUND");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@

import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;

import com.nextcloud.utils.extensions.IntentExtensionsKt;
import com.nextcloud.android.common.ui.util.extensions.WindowExtensionsKt;

import androidx.activity.EdgeToEdge;
import androidx.activity.SystemBarStyle;
import androidx.appcompat.app.AppCompatActivity;

/*
Expand Down Expand Up @@ -46,6 +51,14 @@ public final void setAccountAuthenticatorResult(Bundle result) {
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
// NMC-3936 and NMC-3813 fix
boolean isApiLevel35OrHigher = (Build.VERSION.SDK_INT >= 35);

if (isApiLevel35OrHigher) {
enableEdgeToEdge();
WindowExtensionsKt.addSystemBarPaddings(getWindow());
}

super.onCreate(savedInstanceState);

mAccountAuthenticatorResponse =
Expand All @@ -58,6 +71,11 @@ protected void onCreate(Bundle savedInstanceState) {
}
}

private void enableEdgeToEdge() {
final var style = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT);
EdgeToEdge.enable(this, style, style);
}

/**
* Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present.
*/
Expand Down
Loading