Skip to content

Commit 6844583

Browse files
author
zerox80
committed
Feature: Thumbnail Cache & Login Fixes
Combined update including: 1. **Login State Loss Fix**: - Resolved issue where authentication state was lost during app switching or process death. - Implemented persistent storage for PKCE state (codeVerifier, codeChallenge, oidcState) using SharedPreferences. - Updated LoginActivity launchMode to singleTop and added trampoline logic for OAuth redirects. - Made AuthenticationViewModel PKCE properties mutable for restoration. 2. **Thumbnail Cache Improvements**: - Integrated LruCache for efficient in-memory thumbnail caching. - Implemented null-safe disk cache initialization to prevent crashes. - Improved thumbnail loading performance using Coil. 3. **Bug Fixes**: - Fixed duplicate file listing bug caused by network instability.
1 parent f093c8c commit 6844583

File tree

13 files changed

+428
-164
lines changed

13 files changed

+428
-164
lines changed

opencloudApp/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@
236236
android:name=".presentation.authentication.LoginActivity"
237237
android:exported="true"
238238
android:label="@string/login_label"
239-
android:launchMode="singleTask"
239+
android:launchMode="singleTop"
240240
android:theme="@style/Theme.openCloud.Toolbar">
241241
<intent-filter>
242242
<action android:name="android.intent.action.VIEW" />

opencloudApp/src/main/java/eu/opencloud/android/MainApp.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ class MainApp : Application() {
9999

100100
appContext = applicationContext
101101

102+
// Ensure Logcat shows Timber logs in debug builds
103+
if (BuildConfig.DEBUG) {
104+
try {
105+
Timber.plant(Timber.DebugTree())
106+
} catch (_: Throwable) {
107+
// ignore if already planted
108+
}
109+
}
110+
102111
startLogsIfEnabled()
103112

104113
DebugInjector.injectDebugTools(appContext)

opencloudApp/src/main/java/eu/opencloud/android/datamodel/ThumbnailsCacheManager.java

Lines changed: 212 additions & 48 deletions
Large diffs are not rendered by default.

opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AuthenticationViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ class AuthenticationViewModel(
7272
private val contextProvider: ContextProvider,
7373
) : ViewModel() {
7474

75-
val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
76-
val codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
77-
val oidcState: String = OAuthUtils().generateRandomState()
75+
var codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
76+
var codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
77+
var oidcState: String = OAuthUtils().generateRandomState()
7878

7979
private val _legacyWebfingerHost = MediatorLiveData<Event<UIResult<String>>>()
8080
val legacyWebfingerHost: LiveData<Event<UIResult<String>>> = _legacyWebfingerHost

opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
9393
import timber.log.Timber
9494
import java.io.File
9595

96+
97+
private const val KEY_SERVER_BASE_URL = "KEY_SERVER_BASE_URL"
98+
private const val KEY_OIDC_SUPPORTED = "KEY_OIDC_SUPPORTED"
99+
private const val KEY_CODE_VERIFIER = "KEY_CODE_VERIFIER"
100+
private const val KEY_CODE_CHALLENGE = "KEY_CODE_CHALLENGE"
101+
private const val KEY_OIDC_STATE = "KEY_OIDC_STATE"
102+
96103
class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced {
97104

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

116123
override fun onCreate(savedInstanceState: Bundle?) {
124+
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
125+
if (!isTaskRoot) {
126+
val newIntent = Intent(this, LoginActivity::class.java)
127+
newIntent.data = intent.data
128+
newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
129+
startActivity(newIntent)
130+
finish()
131+
return
132+
}
133+
}
117134
super.onCreate(savedInstanceState)
118135

119136
checkPasscodeEnforced(this)
@@ -136,6 +153,11 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
136153
}
137154
} else {
138155
authTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE)
156+
savedInstanceState.getString(KEY_SERVER_BASE_URL)?.let { serverBaseUrl = it }
157+
oidcSupported = savedInstanceState.getBoolean(KEY_OIDC_SUPPORTED)
158+
savedInstanceState.getString(KEY_CODE_VERIFIER)?.let { authenticationViewModel.codeVerifier = it }
159+
savedInstanceState.getString(KEY_CODE_CHALLENGE)?.let { authenticationViewModel.codeChallenge = it }
160+
savedInstanceState.getString(KEY_OIDC_STATE)?.let { authenticationViewModel.oidcState = it }
139161
}
140162

141163
// UI initialization
@@ -164,6 +186,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
164186
binding.accountUsername.setText(username)
165187
}
166188
}
189+
} else {
190+
// Restore UI state
191+
if (::serverBaseUrl.isInitialized && serverBaseUrl.isNotEmpty()) {
192+
binding.hostUrlInput.setText(serverBaseUrl)
193+
194+
if (authTokenType == BASIC_TOKEN_TYPE) {
195+
showOrHideBasicAuthFields(shouldBeVisible = true)
196+
} else if (authTokenType == OAUTH_TOKEN_TYPE) {
197+
showOrHideBasicAuthFields(shouldBeVisible = false)
198+
}
199+
}
167200
}
168201

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

196229
initLiveDataObservers()
230+
231+
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
232+
if (savedInstanceState == null) {
233+
restoreAuthState()
234+
}
235+
handleGetAuthorizationCodeResponse(intent)
236+
}
197237
}
198238

199239
private fun handleDeepLink() {
200-
if (intent.data != null) {
240+
if (intent.data != null && intent.data?.getQueryParameter("code") == null && intent.data?.getQueryParameter("error") == null) {
201241
authenticationViewModel.launchedFromDeepLink = true
202242
if (getAccounts(baseContext).isNotEmpty()) {
203243
launchFileDisplayActivity()
@@ -469,6 +509,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
469509
setResult(Activity.RESULT_OK, intent)
470510

471511
authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE)
512+
clearAuthState()
472513
}
473514

474515
private fun loginIsLoading() {
@@ -498,6 +539,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
498539
}
499540
}
500541
}
542+
clearAuthState()
501543
}
502544

503545
/**
@@ -553,6 +595,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
553595
)
554596

555597
try {
598+
saveAuthState()
556599
customTabsIntent.launchUrl(
557600
this,
558601
authorizationEndpointUri
@@ -853,6 +896,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
853896
override fun onSaveInstanceState(outState: Bundle) {
854897
super.onSaveInstanceState(outState)
855898
outState.putString(KEY_AUTH_TOKEN_TYPE, authTokenType)
899+
if (::serverBaseUrl.isInitialized) {
900+
outState.putString(KEY_SERVER_BASE_URL, serverBaseUrl)
901+
}
902+
outState.putBoolean(KEY_OIDC_SUPPORTED, oidcSupported)
856903
}
857904

858905
override fun finish() {
@@ -873,4 +920,26 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
873920
override fun optionLockSelected(type: LockType) {
874921
manageOptionLockSelected(type)
875922
}
923+
924+
private fun saveAuthState() {
925+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
926+
prefs.edit().apply {
927+
putString(KEY_CODE_VERIFIER, authenticationViewModel.codeVerifier)
928+
putString(KEY_CODE_CHALLENGE, authenticationViewModel.codeChallenge)
929+
putString(KEY_OIDC_STATE, authenticationViewModel.oidcState)
930+
apply()
931+
}
932+
}
933+
934+
private fun restoreAuthState() {
935+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
936+
prefs.getString(KEY_CODE_VERIFIER, null)?.let { authenticationViewModel.codeVerifier = it }
937+
prefs.getString(KEY_CODE_CHALLENGE, null)?.let { authenticationViewModel.codeChallenge = it }
938+
prefs.getString(KEY_OIDC_STATE, null)?.let { authenticationViewModel.oidcState = it }
939+
}
940+
941+
private fun clearAuthState() {
942+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
943+
prefs.edit().clear().apply()
944+
}
876945
}

opencloudApp/src/main/java/eu/opencloud/android/presentation/files/details/FileDetailsFragment.kt

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import androidx.work.WorkInfo
4040
import com.google.android.material.snackbar.Snackbar
4141
import eu.opencloud.android.MainApp
4242
import eu.opencloud.android.R
43+
import coil.load
4344
import eu.opencloud.android.databinding.FileDetailsFragmentBinding
4445
import eu.opencloud.android.datamodel.ThumbnailsCacheManager
46+
import eu.opencloud.android.presentation.thumbnails.ThumbnailsRequester
4547
import eu.opencloud.android.domain.exceptions.AccountNotFoundException
4648
import eu.opencloud.android.domain.exceptions.InstanceNotConfiguredException
4749
import eu.opencloud.android.domain.exceptions.TooEarlyException
@@ -428,21 +430,10 @@ class FileDetailsFragment : FileFragment() {
428430
}
429431
}
430432
if (ocFile.isImage) {
431-
val tagId = ocFile.remoteId.toString()
432-
var thumbnail: Bitmap? = ThumbnailsCacheManager.getBitmapFromDiskCache(tagId)
433-
if (thumbnail != null && !ocFile.needsToUpdateThumbnail) {
434-
imageView.setImageBitmap(thumbnail)
435-
} else {
436-
// generate new Thumbnail
437-
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(ocFile, imageView)) {
438-
val task = ThumbnailsCacheManager.ThumbnailGenerationTask(imageView, fileDetailsViewModel.getAccount())
439-
if (thumbnail == null) {
440-
thumbnail = ThumbnailsCacheManager.mDefaultImg
441-
}
442-
val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(MainApp.appContext.resources, thumbnail, task)
443-
imageView.setImageDrawable(asyncDrawable)
444-
task.execute(ocFile)
445-
}
433+
imageView.load(ThumbnailsRequester.getPreviewUriForFile(OCFileWithSyncInfo(ocFile, null), fileDetailsViewModel.getAccount()), ThumbnailsRequester.getCoilImageLoader()) {
434+
placeholder(MimetypeIconUtil.getFileTypeIconId(ocFile.mimeType, ocFile.fileName))
435+
error(MimetypeIconUtil.getFileTypeIconId(ocFile.mimeType, ocFile.fileName))
436+
crossfade(true)
446437
}
447438
} else {
448439
// Name of the file, to deduce the icon to use in case the MIME type is not precise enough

0 commit comments

Comments
 (0)