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
2 changes: 1 addition & 1 deletion opencloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
android:name=".presentation.authentication.LoginActivity"
android:exported="true"
android:label="@string/login_label"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:theme="@style/Theme.openCloud.Toolbar">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
15 changes: 13 additions & 2 deletions opencloudApp/src/main/java/eu/opencloud/android/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import android.widget.CheckBox
import androidx.appcompat.app.AlertDialog
import androidx.core.content.pm.PackageInfoCompat
import eu.opencloud.android.data.providers.implementation.OCSharedPreferencesProvider
import eu.opencloud.android.datamodel.ThumbnailsCacheManager

import eu.opencloud.android.db.PreferenceManager
import eu.opencloud.android.dependecyinjection.commonModule
import eu.opencloud.android.dependecyinjection.localDataSourceModule
Expand Down Expand Up @@ -99,6 +99,15 @@ class MainApp : Application() {

appContext = applicationContext

// Ensure Logcat shows Timber logs in debug builds
if (BuildConfig.DEBUG) {
try {
Timber.plant(Timber.DebugTree())
} catch (_: Throwable) {
// ignore if already planted
}
}

startLogsIfEnabled()

DebugInjector.injectDebugTools(appContext)
Expand All @@ -108,7 +117,9 @@ class MainApp : Application() {
SingleSessionManager.setUserAgent(userAgent)

// initialise thumbnails cache on background thread
ThumbnailsCacheManager.InitDiskCacheTask().execute()
// initialise thumbnails cache on background thread



initDependencyInjection()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ class AuthenticationViewModel(
private val contextProvider: ContextProvider,
) : ViewModel() {

val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
val codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
val oidcState: String = OAuthUtils().generateRandomState()
var codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
var codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
var oidcState: String = OAuthUtils().generateRandomState()

private val _legacyWebfingerHost = MediatorLiveData<Event<UIResult<String>>>()
val legacyWebfingerHost: LiveData<Event<UIResult<String>>> = _legacyWebfingerHost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import java.io.File


private const val KEY_SERVER_BASE_URL = "KEY_SERVER_BASE_URL"
private const val KEY_OIDC_SUPPORTED = "KEY_OIDC_SUPPORTED"
private const val KEY_CODE_VERIFIER = "KEY_CODE_VERIFIER"
private const val KEY_CODE_CHALLENGE = "KEY_CODE_CHALLENGE"
private const val KEY_OIDC_STATE = "KEY_OIDC_STATE"

class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced {

private val authenticationViewModel by viewModel<AuthenticationViewModel>()
Expand All @@ -114,6 +121,16 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
private var resultBundle: Bundle? = null

override fun onCreate(savedInstanceState: Bundle?) {
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
if (!isTaskRoot) {
val newIntent = Intent(this, LoginActivity::class.java)
newIntent.data = intent.data
newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(newIntent)
finish()
return
}
}
super.onCreate(savedInstanceState)

checkPasscodeEnforced(this)
Expand All @@ -136,6 +153,11 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
}
} else {
authTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE)
savedInstanceState.getString(KEY_SERVER_BASE_URL)?.let { serverBaseUrl = it }
oidcSupported = savedInstanceState.getBoolean(KEY_OIDC_SUPPORTED)
savedInstanceState.getString(KEY_CODE_VERIFIER)?.let { authenticationViewModel.codeVerifier = it }
savedInstanceState.getString(KEY_CODE_CHALLENGE)?.let { authenticationViewModel.codeChallenge = it }
savedInstanceState.getString(KEY_OIDC_STATE)?.let { authenticationViewModel.oidcState = it }
}

// UI initialization
Expand Down Expand Up @@ -164,6 +186,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
binding.accountUsername.setText(username)
}
}
} else {
// Restore UI state
if (::serverBaseUrl.isInitialized && serverBaseUrl.isNotEmpty()) {
binding.hostUrlInput.setText(serverBaseUrl)

if (authTokenType == BASIC_TOKEN_TYPE) {
showOrHideBasicAuthFields(shouldBeVisible = true)
} else if (authTokenType == OAUTH_TOKEN_TYPE) {
showOrHideBasicAuthFields(shouldBeVisible = false)
}
}
}

binding.root.filterTouchesWhenObscured =
Expand Down Expand Up @@ -194,10 +227,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
accountAuthenticatorResponse?.onRequestContinued()

initLiveDataObservers()

if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
if (savedInstanceState == null) {
restoreAuthState()
}
handleGetAuthorizationCodeResponse(intent)
}
}

private fun handleDeepLink() {
if (intent.data != null) {
if (intent.data != null && intent.data?.getQueryParameter("code") == null && intent.data?.getQueryParameter("error") == null) {
authenticationViewModel.launchedFromDeepLink = true
if (getAccounts(baseContext).isNotEmpty()) {
launchFileDisplayActivity()
Expand Down Expand Up @@ -469,6 +509,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
setResult(Activity.RESULT_OK, intent)

authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE)
clearAuthState()
}

private fun loginIsLoading() {
Expand Down Expand Up @@ -498,6 +539,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
}
}
}
clearAuthState()
}

/**
Expand Down Expand Up @@ -553,6 +595,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
)

try {
saveAuthState()
customTabsIntent.launchUrl(
this,
authorizationEndpointUri
Expand Down Expand Up @@ -853,6 +896,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_AUTH_TOKEN_TYPE, authTokenType)
if (::serverBaseUrl.isInitialized) {
outState.putString(KEY_SERVER_BASE_URL, serverBaseUrl)
}
outState.putBoolean(KEY_OIDC_SUPPORTED, oidcSupported)
}

override fun finish() {
Expand All @@ -873,4 +920,26 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
override fun optionLockSelected(type: LockType) {
manageOptionLockSelected(type)
}

private fun saveAuthState() {
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
prefs.edit().apply {
putString(KEY_CODE_VERIFIER, authenticationViewModel.codeVerifier)
putString(KEY_CODE_CHALLENGE, authenticationViewModel.codeChallenge)
putString(KEY_OIDC_STATE, authenticationViewModel.oidcState)
apply()
}
}

private fun restoreAuthState() {
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
prefs.getString(KEY_CODE_VERIFIER, null)?.let { authenticationViewModel.codeVerifier = it }
prefs.getString(KEY_CODE_CHALLENGE, null)?.let { authenticationViewModel.codeChallenge = it }
prefs.getString(KEY_OIDC_STATE, null)?.let { authenticationViewModel.oidcState = it }
}

private fun clearAuthState() {
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
prefs.edit().clear().apply()
}
}
3 changes: 1 addition & 2 deletions opencloudApp/src/main/res/drawable/ic_action_create_dir.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
Expand Down
3 changes: 1 addition & 2 deletions opencloudApp/src/main/res/drawable/ic_action_create_file.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
android:height="128dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ class ClientManager(
}
} else {
Timber.d("Reusing anonymous client for ${safeClient.baseUri}")
safeClient
safeClient.apply {
credentials = openCloudCredentials
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package eu.opencloud.android.data

import android.accounts.AccountManager
import android.content.Context
import eu.opencloud.android.data.providers.SharedPreferencesProvider
import eu.opencloud.android.lib.common.ConnectionValidator
import eu.opencloud.android.lib.common.authentication.OpenCloudCredentialsFactory
import io.mockk.mockk
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class ClientManagerTest {

private val accountManager: AccountManager = mockk()
private val preferencesProvider: SharedPreferencesProvider = mockk()
private val context: Context = mockk(relaxed = true)
private val connectionValidator: ConnectionValidator = mockk()
private lateinit var clientManager: ClientManager

@Before
fun setUp() {
mockkStatic(android.net.Uri::class)
val uriMock = mockk<android.net.Uri>()
io.mockk.every { android.net.Uri.parse(any()) } returns uriMock
io.mockk.every { uriMock.toString() } returns "https://demo.owncloud.com"

clientManager = ClientManager(
accountManager,
preferencesProvider,
context,
"eu.opencloud.android.account",
connectionValidator
)
}

@org.junit.After
fun tearDown() {
io.mockk.unmockkStatic(android.net.Uri::class)
}

@Test
fun `getClientForAnonymousCredentials reuses client and resets credentials`() {
val url = "https://demo.owncloud.com"
val mockClient = mockk<eu.opencloud.android.lib.common.OpenCloudClient>(relaxed = true)
val uriMock = android.net.Uri.parse(url)

io.mockk.every { mockClient.baseUri } returns uriMock

// Inject mock client into clientManager
val field = ClientManager::class.java.getDeclaredField("openCloudClient")
field.isAccessible = true
field.set(clientManager, mockClient)

// Call method - should reuse mockClient
val resultClient = clientManager.getClientForAnonymousCredentials(url, false)

assertEquals("Client should be reused", mockClient, resultClient)

// Verify credentials were set
io.mockk.verify { mockClient.credentials = any() }
}
}