From 55a6929af8f8be4e618c081903f154af1638a973 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Thu, 11 Apr 2024 14:45:43 +0200 Subject: [PATCH 01/29] Thumbail Generation study - inconsistent changes --- .../data/cloud/crypto/CryptoImplDecorator.kt | 63 ++++++++++++++++++ .../presentation/model/CloudNodeModel.kt | 3 + .../presenter/BrowseFilesPresenter.kt | 4 ++ .../presenter/VaultListPresenter.kt | 64 +++++++++---------- .../ui/adapter/BrowseFilesAdapter.kt | 52 ++++++++++++++- .../ui/fragment/BrowseFilesFragment.kt | 2 + .../ui/fragment/SettingsFragment.kt | 15 ++++- presentation/src/main/res/values/arrays.xml | 11 ++++ presentation/src/main/res/xml/preferences.xml | 12 ++++ .../util/SharedPreferencesHandler.kt | 9 +++ 10 files changed, 201 insertions(+), 34 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 6e5c0ad83..9dc31d156 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -1,6 +1,11 @@ package org.cryptomator.data.cloud.crypto import android.content.Context +import android.graphics.BitmapFactory +import android.media.ThumbnailUtils +import android.os.Build +import android.util.Size +import com.tomclaw.cache.DiskLruCache import org.cryptomator.cryptolib.api.Cryptor import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel @@ -24,18 +29,23 @@ import org.cryptomator.domain.usecases.cloud.DownloadState import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState +import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil import java.io.ByteArrayOutputStream import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.OutputStream +import java.nio.Buffer import java.nio.ByteBuffer import java.nio.channels.Channels import java.util.LinkedList import java.util.Queue import java.util.UUID import java.util.function.Supplier +import okio.appendingSink +import timber.log.Timber abstract class CryptoImplDecorator( @@ -50,6 +60,19 @@ abstract class CryptoImplDecorator( @Volatile private var root: RootCryptoFolder? = null + private var diskLruCache: DiskLruCache? = null + private fun createLruCache(cacheSize: Int): Boolean { + if (diskLruCache == null) { + diskLruCache = try { + DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.DROPBOX), cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache") + return false + } + } + return true + } + @Throws(BackendException::class) abstract fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder @@ -309,8 +332,25 @@ abstract class CryptoImplDecorator( @Throws(BackendException::class) fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { val ciphertextFile = cryptoFile.cloudFile + +// // prepare LRU cache +// val s = SharedPreferencesHandler(context) +// +// // cacheKey +// val cacheKey = "$cryptoFile.name${cryptoFile.hashCode()}" +// +// // generating thumbnail +// var genThumbnail = false; +// // TODO: externalize string +// if(s.useLruCache() && !s.generateThumbnails().equals("Never") && createLruCache(s.lruCacheSize())) { +// genThumbnail = true; +// } + + val thumbnailTmp = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + // DiskLruCache try { val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) +// val thumbnailTmpSink = thumbnailTmp.appendingSink() progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) try { Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> @@ -322,6 +362,10 @@ abstract class CryptoImplDecorator( while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { buff.flip() data.write(buff.array(), 0, buff.remaining()) + + // TODO: write into the tmp file + // thumbnailTmpSink.write(buff, buff.remaining()) + decrypted += read.toLong() progressAware .onProgress( @@ -334,12 +378,31 @@ abstract class CryptoImplDecorator( } } } finally { +// thumbnailTmpSink.close() encryptedTmpFile.delete() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } } catch (e: IOException) { throw FatalBackendException(e) } + + // store it in cloud-related LRU cache +// if(genThumbnail) { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +// ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) +// } else { +// ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100); +// } +// +// +// try { +// diskLruCache?.let { +// LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailTmp) +// } ?: Timber.tag("CryptoImpl").e("Failed to store item in LRU cache") +// } catch (e: IOException) { +// Timber.tag("CryptoImpl").e(e, "Failed to write downloaded file in LRU cache") +// } +// } } @Throws(BackendException::class, IOException::class) diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt index 4e07dd4af..788da69af 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt @@ -1,5 +1,6 @@ package org.cryptomator.presentation.model +import android.graphics.Bitmap import org.cryptomator.domain.CloudNode import java.io.Serializable @@ -8,6 +9,8 @@ abstract class CloudNodeModel internal constructor(private val cl var oldName: String? = null var progress: ProgressModel? = null var isSelected = false + var thumbnail: Int = 0 // reference to a file in LRU Cache cloud-related + val name: String get() = cloudNode.name val simpleName: String diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index fa9e235f6..6299ff1f1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter import android.content.ActivityNotFoundException import android.content.Intent +import android.graphics.BitmapFactory import android.net.Uri import android.provider.DocumentsContract import android.widget.Toast @@ -374,6 +375,7 @@ class BrowseFilesPresenter @Inject constructor( // private fun handleSuccessAfterReadingFiles(files: List, actionAfterDownload: String) { try { + // generateThumbnailUseCase.retrieveAndSetCloudNodeModel() if (Intent.ACTION_VIEW == actionAfterDownload) { viewFile(cloudFileModelMapper.toModel(files[0])) } else { @@ -513,6 +515,8 @@ class BrowseFilesPresenter @Inject constructor( // ) } else if (!lowerFileName.endsWith(".gif") && isImageMediaType(cloudFile.name)) { val cloudFileNodes = previewCloudFileNodes + cloudFileNodes.get(cloudFileNodes.indexOf(cloudFile)).thumbnail = R.drawable.happy_doggino + val imagePreviewStore = ImagePreviewFilesStore( // cloudFileNodes, // cloudFileNodes.indexOf(cloudFile) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 6750b1241..047e6df00 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -123,42 +123,42 @@ class VaultListPresenter @Inject constructor( // sharedPreferencesHandler.vaultsRemovedDuringMigration(null) } - checkLicense() + // checkLicense() checkPermissions() } - private fun checkLicense() { - if (BuildConfig.FLAVOR == "apkstore" || BuildConfig.FLAVOR == "fdroid" || BuildConfig.FLAVOR == "lite") { - licenseCheckUseCase // - .withLicense("") // - .run(object : NoOpResultHandler() { - override fun onSuccess(licenseCheck: LicenseCheck) { - if (BuildConfig.FLAVOR == "apkstore" && sharedPreferencesHandler.doUpdate()) { - checkForAppUpdates() - } - } - - override fun onError(e: Throwable) { - val license = if (e is LicenseNotValidException) { - e.license - } else { - "" - } - val intent = Intent(context(), LicenseCheckActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - intent.data = Uri.parse(String.format("app://cryptomator/%s", license)) - - try { - context().startActivity(intent) - } catch (e: ActivityNotFoundException) { - Toast.makeText(context(), "Please contact the support.", Toast.LENGTH_LONG).show() - finish() - } - } - }) - } - } +// private fun checkLicense() { +// if (BuildConfig.FLAVOR == "apkstore" || BuildConfig.FLAVOR == "fdroid" || BuildConfig.FLAVOR == "lite") { +// licenseCheckUseCase // +// .withLicense("") // +// .run(object : NoOpResultHandler() { +// override fun onSuccess(licenseCheck: LicenseCheck) { +// if (BuildConfig.FLAVOR == "apkstore" && sharedPreferencesHandler.doUpdate()) { +// checkForAppUpdates() +// } +// } +// +// override fun onError(e: Throwable) { +// val license = if (e is LicenseNotValidException) { +// e.license +// } else { +// "" +// } +// val intent = Intent(context(), LicenseCheckActivity::class.java) +// intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK +// intent.data = Uri.parse(String.format("app://cryptomator/%s", license)) +// +// try { +// context().startActivity(intent) +// } catch (e: ActivityNotFoundException) { +// Toast.makeText(context(), "Please contact the support.", Toast.LENGTH_LONG).show() +// finish() +// } +// } +// }) +// } +// } private fun checkForAppUpdates() { if (networkConnectionCheck.isPresent) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 7b05f62fc..9a3ee6011 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -1,7 +1,14 @@ package org.cryptomator.presentation.ui.adapter +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.ThumbnailUtils +import android.os.Build import android.os.PatternMatcher import android.view.LayoutInflater +import android.util.Size import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -30,6 +37,11 @@ import org.cryptomator.presentation.util.FileSizeHelper import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.ResourceHelper.Companion.getDrawable import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.MimeType +import org.cryptomator.util.file.MimeTypes +import java.io.File +import java.io.IOException import javax.inject.Inject class BrowseFilesAdapter @Inject @@ -37,7 +49,9 @@ constructor( private val dateHelper: DateHelper, // private val fileSizeHelper: FileSizeHelper, // private val fileUtil: FileUtil, // - private val sharedPreferencesHandler: SharedPreferencesHandler + private val sharedPreferencesHandler: SharedPreferencesHandler, // + private val context : Context, // + private val mimeTypes: MimeTypes // ) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>(CloudNodeModelNameAZComparator()), FastScrollRecyclerView.SectionedAdapter { private var chooseCloudNodeSettings: ChooseCloudNodeSettings? = null @@ -122,6 +136,8 @@ constructor( private var bound: CloudNodeModel<*>? = null +// private var diskLruCache: DiskLruCache? = null + override fun bind(position: Int) { bound = getItem(position) bound?.let { internalBind(it) } @@ -134,8 +150,42 @@ constructor( bindFileOrFolder(node) } +// private fun createLruCache(cacheSize: Int): Boolean { +// if (diskLruCache == null) { +// diskLruCache = try { +// DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.GOOGLE_DRIVE), cacheSize.toLong()) +// } catch (e: IOException) { +// Timber.tag("GoogleDriveImpl").e(e, "Failed to setup LRU cache") +// return false +// } +// } +// return true +// } + private fun bindNodeImage(node: CloudNodeModel<*>) { binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) +// val s = SharedPreferencesHandler() +// if(s.useLruCache() && !s.generateThumbnails().equals("Never") && createLruCache(s.lruCacheSize())) { +// +// } + +// node.toCloudNode().cloud.id() + if (isImageMediaType(node.name) && node.thumbnail != 0) { + itemView.cloudNodeImage.setImageResource(node.thumbnail) +// val thumbnail = retrieveThumbnailBitmap() +// itemView.cloudNodeImage.setImageBitmap(thumbnail) + } else { + itemView.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) + } + } + + private fun retrieveThumbnailBitmap() : Bitmap { + TODO("to implement!") + + } + + private fun isImageMediaType(filename: String): Boolean { + return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" } private fun bindCloudNodeImage(cloudNodeModel: CloudNodeModel<*>): Int { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt index decd01a0e..3cf10116e 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt @@ -8,6 +8,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.widget.RelativeLayout import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import org.cryptomator.domain.CloudNode @@ -27,6 +28,7 @@ import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.presenter.BrowseFilesPresenter import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset +import org.cryptomator.util.SharedPreferencesHandler import java.util.Optional import javax.inject.Inject diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index 50dc8c545..2a7b942a5 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -28,6 +28,7 @@ import org.cryptomator.presentation.ui.dialog.DisableSecureScreenDisclaimerDialo import org.cryptomator.presentation.ui.dialog.MicrosoftWorkaroundDisclaimerDialog import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.SharedPreferencesHandler.Companion.CRYPTOMATOR_VARIANTS +import org.cryptomator.util.SharedPreferencesHandler.Companion.THUMBNAIL_GENERATION import org.cryptomator.util.file.LruFileCacheUtil import java.lang.Boolean.FALSE import java.lang.Boolean.TRUE @@ -47,6 +48,7 @@ class SettingsFragment : PreferenceFragmentCompat() { setupAppVersion() setupLruCacheSize() setupLicense() + setupThumbnailGeneration() setupCryptomatorVariants() } @@ -109,6 +111,11 @@ class SettingsFragment : PreferenceFragmentCompat() { true } + private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + // TODO ... + true + } + private fun activity(): SettingsActivity = this.activity as SettingsActivity private fun isBiometricAuthenticationNotAvailableRemovePreference() { @@ -140,9 +147,13 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + private fun setupThumbnailGeneration() { + val preference = findPreference(THUMBNAIL_GENERATION) as Preference? + // TODO ... + } + private fun setupLruCacheSize() { val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) as Preference? - val size = LruFileCacheUtil(requireContext()).totalSize() val readableSize: String = if (size > 0) { @@ -245,6 +256,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD_VAULT) as Preference?)?.intent = Intent(context, AutoUploadChooseVaultActivity::class.java) (findPreference(SharedPreferencesHandler.LICENSES_ACTIVITY) as Preference?)?.intent = Intent(context, LicensesActivity::class.java) + (findPreference(SharedPreferencesHandler.THUMBNAIL_GENERATION) as Preference?)?.onPreferenceChangeListener = thumbnailGenerationChangeListener } fun deactivateDebugMode() { @@ -327,6 +339,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private const val UPDATE_INTERVAL_ITEM_KEY = "updateInterval" private const val DISPLAY_LRU_CACHE_SIZE_ITEM_KEY = "displayLruCacheSize" private const val LRU_CACHE_CLEAR_ITEM_KEY = "lruCacheClear" + private const val THUMBNAIL_GENERATION = "thumbnailGeneration" } } diff --git a/presentation/src/main/res/values/arrays.xml b/presentation/src/main/res/values/arrays.xml index e2f96320b..e1fbbca51 100644 --- a/presentation/src/main/res/values/arrays.xml +++ b/presentation/src/main/res/values/arrays.xml @@ -42,6 +42,17 @@ 1000 5000 + + @string/thumbnail_generation_never + @string/thumbnail_generation_file + @string/thumbnail_generation_folder + + + "NEVER" + "PER_FILE" + "PER_FOLDER" + + @string/update_interval_1d @string/update_interval_never diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 3a4fdaaa6..403b44895 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -120,6 +120,18 @@ + + + + + Unit) { From f1ca2801f5b7fc363fc579216d672d98f7ebb2a2 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 13 Apr 2024 16:51:26 +0200 Subject: [PATCH 02/29] Added thumbnail File in CryptoFile and logic for storing and retrieving thumbnail from the DiskLruCache --- .../data/cloud/crypto/CryptoFile.kt | 3 + .../data/cloud/crypto/CryptoImplDecorator.kt | 134 ++++++++++++------ .../cloud/crypto/CryptoImplVaultFormat7.kt | 17 ++- .../presentation/model/CloudFileModel.kt | 3 + .../presentation/model/CloudNodeModel.kt | 3 +- .../presenter/BrowseFilesPresenter.kt | 2 - .../presenter/VaultListPresenter.kt | 64 ++++----- .../ui/adapter/BrowseFilesAdapter.kt | 34 +---- .../ui/bottomsheet/FileSettingsBottomSheet.kt | 10 ++ .../ui/fragment/BrowseFilesFragment.kt | 1 - presentation/src/main/res/values/strings.xml | 6 + 11 files changed, 162 insertions(+), 115 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt index a8284f602..143fbb563 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt @@ -2,6 +2,7 @@ package org.cryptomator.data.cloud.crypto import org.cryptomator.domain.Cloud import org.cryptomator.domain.CloudFile +import java.io.File import java.util.Date class CryptoFile( @@ -12,6 +13,8 @@ class CryptoFile( val cloudFile: CloudFile ) : CloudFile, CryptoNode { + var thumbnail : File? = null + override val cloud: Cloud? get() = parent.cloud diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 9dc31d156..3d0282683 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -1,6 +1,7 @@ package org.cryptomator.data.cloud.crypto import android.content.Context +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.ThumbnailUtils import android.os.Build @@ -14,6 +15,7 @@ import org.cryptomator.domain.Cloud import org.cryptomator.domain.CloudFile import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.CloudNode +import org.cryptomator.domain.CloudType import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException import org.cryptomator.domain.exception.EmptyDirFileException @@ -37,14 +39,12 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.OutputStream -import java.nio.Buffer import java.nio.ByteBuffer import java.nio.channels.Channels import java.util.LinkedList import java.util.Queue import java.util.UUID import java.util.function.Supplier -import okio.appendingSink import timber.log.Timber @@ -60,17 +60,46 @@ abstract class CryptoImplDecorator( @Volatile private var root: RootCryptoFolder? = null - private var diskLruCache: DiskLruCache? = null - private fun createLruCache(cacheSize: Int): Boolean { - if (diskLruCache == null) { - diskLruCache = try { - DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.DROPBOX), cacheSize.toLong()) - } catch (e: IOException) { - Timber.tag("DropboxImpl").e(e, "Failed to setup LRU cache") - return false + private val sharedPreferencesHandler = SharedPreferencesHandler(context) + + private var diskLruCache: MutableMap = mutableMapOf() + + protected fun getLruCacheFor(type : CloudType): DiskLruCache? { + return getOrCreateLruCache(sharedPreferencesHandler.lruCacheSize(), dispatchCloud(type)!!) // unwrap should be safe! + } + private fun getOrCreateLruCache(cacheSize: Int, key : LruFileCacheUtil.Cache): DiskLruCache? { + if(diskLruCache[key] == null) { + diskLruCache[key] = createLruCache(LruFileCacheUtil(context).resolve(key), cacheSize.toLong()) + } + return diskLruCache[key] + } + + private fun createLruCache(where: File, size: Long): DiskLruCache? { + return try { + DiskLruCache.create(where, size) + } catch (e: IOException) { + Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name") + null + } + } + + + private fun dispatchCloud(type : CloudType) : LruFileCacheUtil.Cache? { + return when (type) { + CloudType.DROPBOX -> LruFileCacheUtil.Cache.DROPBOX + CloudType.GOOGLE_DRIVE -> LruFileCacheUtil.Cache.GOOGLE_DRIVE + CloudType.ONEDRIVE -> LruFileCacheUtil.Cache.ONEDRIVE + CloudType.PCLOUD -> LruFileCacheUtil.Cache.PCLOUD + CloudType.WEBDAV -> LruFileCacheUtil.Cache.WEBDAV + CloudType.S3 -> LruFileCacheUtil.Cache.S3 + CloudType.LOCAL -> LruFileCacheUtil.Cache.DROPBOX // TODO: where!!!! + // CloudType.CRYPTO -> ... + else -> { + // it should be impossible to enter here, a cloud file could not be another type... + Timber.tag("CryptoImplDecorator").e("Unable to choose which cloud-cache") + null } } - return true } @Throws(BackendException::class) @@ -333,24 +362,26 @@ abstract class CryptoImplDecorator( fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { val ciphertextFile = cryptoFile.cloudFile -// // prepare LRU cache -// val s = SharedPreferencesHandler(context) -// -// // cacheKey -// val cacheKey = "$cryptoFile.name${cryptoFile.hashCode()}" -// -// // generating thumbnail -// var genThumbnail = false; -// // TODO: externalize string -// if(s.useLruCache() && !s.generateThumbnails().equals("Never") && createLruCache(s.lruCacheSize())) { -// genThumbnail = true; -// } - - val thumbnailTmp = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) - // DiskLruCache + val diskCache = getLruCacheFor(cryptoFile.cloudFile.cloud!!.type()!!) + val cacheKey = ciphertextFile.path.hashCode().toString().substring(3) // TODO: fare la stessa cacheKey nella list + + var genThumbnail = false + if( sharedPreferencesHandler.useLruCache() && + !sharedPreferencesHandler.generateThumbnails().equals("Never") && // TODO: externalize string + diskCache != null) { + genThumbnail = true + } + + // TODO: solo se e' un file immagine!!! + val thumbnailTmp : File try { + + // cloudContentRepository.read(file, encryptedTmpFile, encryptedData, ...) + // file appena letto dalla rete, portato in cache ancora cifrato! val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) -// val thumbnailTmpSink = thumbnailTmp.appendingSink() + thumbnailTmp = File.createTempFile(encryptedTmpFile.nameWithoutExtension, ".tmp", internalCache) + + val thumbnailTmpOutputStream = thumbnailTmp.outputStream() progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) try { Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> @@ -362,9 +393,7 @@ abstract class CryptoImplDecorator( while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { buff.flip() data.write(buff.array(), 0, buff.remaining()) - - // TODO: write into the tmp file - // thumbnailTmpSink.write(buff, buff.remaining()) + thumbnailTmpOutputStream.write(buff.array(), 0, buff.remaining()) decrypted += read.toLong() progressAware @@ -378,7 +407,8 @@ abstract class CryptoImplDecorator( } } } finally { -// thumbnailTmpSink.close() + thumbnailTmpOutputStream.flush() + thumbnailTmpOutputStream.close() encryptedTmpFile.delete() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } @@ -387,22 +417,32 @@ abstract class CryptoImplDecorator( } // store it in cloud-related LRU cache -// if(genThumbnail) { -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) -// } else { -// ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100); -// } -// -// -// try { -// diskLruCache?.let { -// LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailTmp) -// } ?: Timber.tag("CryptoImpl").e("Failed to store item in LRU cache") -// } catch (e: IOException) { -// Timber.tag("CryptoImpl").e(e, "Failed to write downloaded file in LRU cache") -// } -// } + val thumbnailFile : File + if(genThumbnail) { + + // generate the Bitmap (in memory) + val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) + } else { + ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) + } + + // write the thumbnail in a file (on disk) + thumbnailFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) + + try { + diskCache?.let { + // store File to LruCache (on disk) + LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile) + } ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("CryptoImplDecorator").e(e, "Failed to write downloaded file in LRU cache") + } + + thumbnailFile.delete() + } + thumbnailTmp.delete() } @Throws(BackendException::class, IOException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 4128ccc7a..025a308f0 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -166,6 +166,21 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } }.map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) + }.map { cryptoNode -> + + // if present, associate cached-thumbnail to the Cryptofile + if(cryptoNode is CryptoFile) { + val cacheKey = cryptoNode.cloudFile.path.hashCode().toString().substring(3) + + val diskCache = super.getLruCacheFor(cryptoNode.cloudFile.cloud!!.type()!!) + diskCache?.let { + val cacheFile = it[cacheKey] + if (cacheFile != null) { + cryptoNode.thumbnail = cacheFile + } + } + } + cryptoNode }.toList().filterNotNull() } @@ -493,7 +508,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { cryptoFile, // cloudContentRepository.write( // targetFile, // - data.decorate(from(encryptedTmpFile)), + data.decorate(from(encryptedTmpFile)), // UploadFileReplacingProgressAware(cryptoFile, progressAware), // replace, // encryptedTmpFile.length() diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt index e2fa8a71b..a2b1a15ef 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt @@ -1,14 +1,17 @@ package org.cryptomator.presentation.model +import org.cryptomator.data.cloud.crypto.CryptoFile import org.cryptomator.domain.CloudFile import org.cryptomator.domain.usecases.ResultRenamed import org.cryptomator.presentation.util.FileIcon +import java.io.File import java.util.Date class CloudFileModel(cloudFile: CloudFile, val icon: FileIcon) : CloudNodeModel(cloudFile) { val modified: Date? = cloudFile.modified val size: Long? = cloudFile.size + var thumbnail : File? = if (cloudFile is CryptoFile) cloudFile.thumbnail else null constructor(cloudFileRenamed: ResultRenamed, icon: FileIcon) : this(cloudFileRenamed.value(), icon) { oldName = cloudFileRenamed.oldName diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt index 788da69af..05957766c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt @@ -9,8 +9,7 @@ abstract class CloudNodeModel internal constructor(private val cl var oldName: String? = null var progress: ProgressModel? = null var isSelected = false - var thumbnail: Int = 0 // reference to a file in LRU Cache cloud-related - + val name: String get() = cloudNode.name val simpleName: String diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index 6299ff1f1..9f2cd00cf 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -375,7 +375,6 @@ class BrowseFilesPresenter @Inject constructor( // private fun handleSuccessAfterReadingFiles(files: List, actionAfterDownload: String) { try { - // generateThumbnailUseCase.retrieveAndSetCloudNodeModel() if (Intent.ACTION_VIEW == actionAfterDownload) { viewFile(cloudFileModelMapper.toModel(files[0])) } else { @@ -515,7 +514,6 @@ class BrowseFilesPresenter @Inject constructor( // ) } else if (!lowerFileName.endsWith(".gif") && isImageMediaType(cloudFile.name)) { val cloudFileNodes = previewCloudFileNodes - cloudFileNodes.get(cloudFileNodes.indexOf(cloudFile)).thumbnail = R.drawable.happy_doggino val imagePreviewStore = ImagePreviewFilesStore( // cloudFileNodes, // diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 047e6df00..6750b1241 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -123,42 +123,42 @@ class VaultListPresenter @Inject constructor( // sharedPreferencesHandler.vaultsRemovedDuringMigration(null) } - // checkLicense() + checkLicense() checkPermissions() } -// private fun checkLicense() { -// if (BuildConfig.FLAVOR == "apkstore" || BuildConfig.FLAVOR == "fdroid" || BuildConfig.FLAVOR == "lite") { -// licenseCheckUseCase // -// .withLicense("") // -// .run(object : NoOpResultHandler() { -// override fun onSuccess(licenseCheck: LicenseCheck) { -// if (BuildConfig.FLAVOR == "apkstore" && sharedPreferencesHandler.doUpdate()) { -// checkForAppUpdates() -// } -// } -// -// override fun onError(e: Throwable) { -// val license = if (e is LicenseNotValidException) { -// e.license -// } else { -// "" -// } -// val intent = Intent(context(), LicenseCheckActivity::class.java) -// intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK -// intent.data = Uri.parse(String.format("app://cryptomator/%s", license)) -// -// try { -// context().startActivity(intent) -// } catch (e: ActivityNotFoundException) { -// Toast.makeText(context(), "Please contact the support.", Toast.LENGTH_LONG).show() -// finish() -// } -// } -// }) -// } -// } + private fun checkLicense() { + if (BuildConfig.FLAVOR == "apkstore" || BuildConfig.FLAVOR == "fdroid" || BuildConfig.FLAVOR == "lite") { + licenseCheckUseCase // + .withLicense("") // + .run(object : NoOpResultHandler() { + override fun onSuccess(licenseCheck: LicenseCheck) { + if (BuildConfig.FLAVOR == "apkstore" && sharedPreferencesHandler.doUpdate()) { + checkForAppUpdates() + } + } + + override fun onError(e: Throwable) { + val license = if (e is LicenseNotValidException) { + e.license + } else { + "" + } + val intent = Intent(context(), LicenseCheckActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + intent.data = Uri.parse(String.format("app://cryptomator/%s", license)) + + try { + context().startActivity(intent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(context(), "Please contact the support.", Toast.LENGTH_LONG).show() + finish() + } + } + }) + } + } private fun checkForAppUpdates() { if (networkConnectionCheck.isPresent) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 9a3ee6011..d7ab194eb 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -50,7 +50,6 @@ constructor( private val fileSizeHelper: FileSizeHelper, // private val fileUtil: FileUtil, // private val sharedPreferencesHandler: SharedPreferencesHandler, // - private val context : Context, // private val mimeTypes: MimeTypes // ) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>(CloudNodeModelNameAZComparator()), FastScrollRecyclerView.SectionedAdapter { @@ -136,8 +135,6 @@ constructor( private var bound: CloudNodeModel<*>? = null -// private var diskLruCache: DiskLruCache? = null - override fun bind(position: Int) { bound = getItem(position) bound?.let { internalBind(it) } @@ -150,40 +147,17 @@ constructor( bindFileOrFolder(node) } -// private fun createLruCache(cacheSize: Int): Boolean { -// if (diskLruCache == null) { -// diskLruCache = try { -// DiskLruCache.create(LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.GOOGLE_DRIVE), cacheSize.toLong()) -// } catch (e: IOException) { -// Timber.tag("GoogleDriveImpl").e(e, "Failed to setup LRU cache") -// return false -// } -// } -// return true -// } - private fun bindNodeImage(node: CloudNodeModel<*>) { binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) -// val s = SharedPreferencesHandler() -// if(s.useLruCache() && !s.generateThumbnails().equals("Never") && createLruCache(s.lruCacheSize())) { -// -// } - -// node.toCloudNode().cloud.id() - if (isImageMediaType(node.name) && node.thumbnail != 0) { - itemView.cloudNodeImage.setImageResource(node.thumbnail) -// val thumbnail = retrieveThumbnailBitmap() -// itemView.cloudNodeImage.setImageBitmap(thumbnail) + + if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail != null) { + val bitmap = BitmapFactory.decodeFile(node.thumbnail!!.absolutePath) + itemView.cloudNodeImage.setImageBitmap(bitmap) } else { itemView.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) } } - private fun retrieveThumbnailBitmap() : Bitmap { - TODO("to implement!") - - } - private fun isImageMediaType(filename: String): Boolean { return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt index 62c1aa78a..8b573e8ac 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt @@ -1,5 +1,6 @@ package org.cryptomator.presentation.ui.bottomsheet +import android.graphics.BitmapFactory import android.os.Bundle import android.view.View import org.cryptomator.generator.BottomSheet @@ -28,6 +29,15 @@ class FileSettingsBottomSheet : BaseBottomSheet1 GB 5 GB + Never + Per File + Per Folder + Style Automatic (follow system) @@ -641,5 +645,7 @@ Once a day @string/lock_timeout_never + Thumbnails + Thumbnail generation From ef395844ffc7b1f79e648c935993a0c0dfe48ee6 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sat, 13 Apr 2024 19:42:00 +0200 Subject: [PATCH 03/29] Remove thumbnail from cache, add check if thumbnail exists only for images --- .../cloud/crypto/CryptoImplVaultFormat7.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 025a308f0..02a522366 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -27,6 +27,9 @@ import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState +import org.cryptomator.util.file.MimeType +import org.cryptomator.util.file.MimeTypeMap +import org.cryptomator.util.file.MimeTypes import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -169,7 +172,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { }.map { cryptoNode -> // if present, associate cached-thumbnail to the Cryptofile - if(cryptoNode is CryptoFile) { + if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = cryptoNode.cloudFile.path.hashCode().toString().substring(3) val diskCache = super.getLruCacheFor(cryptoNode.cloudFile.cloud!!.type()!!) @@ -184,6 +187,11 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { }.toList().filterNotNull() } + private fun isImageMediaType(filename: String): Boolean { + val mimeTypes = MimeTypes(MimeTypeMap()) //TODO not efficient move creation of mimetypes + return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" + } + @Throws(BackendException::class) private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudNode): CryptoNode? { var ciphertextName = cloudNode.name @@ -464,6 +472,17 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } else { cloudContentRepository.delete(node.cloudFile) } + + // Delete thumbnail file from cache + val diskCache = super.getLruCacheFor(node.cloudFile.cloud!!.type()!!) + val cacheKey = node.cloudFile.path.hashCode().toString().substring(3) + + diskCache?.let { + val cacheFile = it[cacheKey] + if (cacheFile != null) { + diskCache.delete(cacheKey) + } + } } } From 53779c4e5465849f8d1aa15c4a9f07b3b17e8107 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sun, 14 Apr 2024 12:22:23 +0200 Subject: [PATCH 04/29] Generate thumbnails only for images --- .../data/cloud/crypto/CryptoImplDecorator.kt | 15 +++++++++++++-- .../data/cloud/crypto/CryptoImplVaultFormat7.kt | 5 ----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 3d0282683..95475551d 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -33,6 +33,9 @@ import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.file.LruFileCacheUtil +import org.cryptomator.util.file.MimeType +import org.cryptomator.util.file.MimeTypeMap +import org.cryptomator.util.file.MimeTypes import java.io.ByteArrayOutputStream import java.io.File import java.io.FileInputStream @@ -64,6 +67,8 @@ abstract class CryptoImplDecorator( private var diskLruCache: MutableMap = mutableMapOf() + private val mimeTypes = MimeTypes(MimeTypeMap()) + protected fun getLruCacheFor(type : CloudType): DiskLruCache? { return getOrCreateLruCache(sharedPreferencesHandler.lruCacheSize(), dispatchCloud(type)!!) // unwrap should be safe! } @@ -368,7 +373,9 @@ abstract class CryptoImplDecorator( var genThumbnail = false if( sharedPreferencesHandler.useLruCache() && !sharedPreferencesHandler.generateThumbnails().equals("Never") && // TODO: externalize string - diskCache != null) { + diskCache != null && // + isImageMediaType(cryptoFile.name) + ) { genThumbnail = true } @@ -428,7 +435,7 @@ abstract class CryptoImplDecorator( } // write the thumbnail in a file (on disk) - thumbnailFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) + thumbnailFile = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) try { @@ -445,6 +452,10 @@ abstract class CryptoImplDecorator( thumbnailTmp.delete() } + protected fun isImageMediaType(filename: String): Boolean { + return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" + } + @Throws(BackendException::class, IOException::class) private fun readToTmpFile(cryptoFile: CryptoFile, file: CloudFile, progressAware: ProgressAware): File { val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 02a522366..f5629b672 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -187,11 +187,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { }.toList().filterNotNull() } - private fun isImageMediaType(filename: String): Boolean { - val mimeTypes = MimeTypes(MimeTypeMap()) //TODO not efficient move creation of mimetypes - return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" - } - @Throws(BackendException::class) private fun ciphertextToCleartextNode(cryptoFolder: CryptoFolder, dirId: String, cloudNode: CloudNode): CryptoNode? { var ciphertextName = cloudNode.name From e69bb98e09f21be2c05c5c39a7fd5acd5728887d Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Fri, 19 Apr 2024 09:44:09 +0200 Subject: [PATCH 05/29] Removed unused imports --- .../cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index f5629b672..e896e2b44 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -27,9 +27,6 @@ import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState -import org.cryptomator.util.file.MimeType -import org.cryptomator.util.file.MimeTypeMap -import org.cryptomator.util.file.MimeTypes import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream From ce1c98993087d3ef0dd769cc66c2275413b5e78a Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sun, 21 Apr 2024 22:18:16 +0200 Subject: [PATCH 06/29] Add local LRU cache and first refactor --- .../data/cloud/crypto/CryptoImplDecorator.kt | 78 ++++++++++--------- .../cloud/crypto/CryptoImplVaultFormat7.kt | 4 +- .../cryptomator/util/file/LruFileCacheUtil.kt | 3 +- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 95475551d..1d3811dac 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -97,7 +97,7 @@ abstract class CryptoImplDecorator( CloudType.PCLOUD -> LruFileCacheUtil.Cache.PCLOUD CloudType.WEBDAV -> LruFileCacheUtil.Cache.WEBDAV CloudType.S3 -> LruFileCacheUtil.Cache.S3 - CloudType.LOCAL -> LruFileCacheUtil.Cache.DROPBOX // TODO: where!!!! + CloudType.LOCAL -> LruFileCacheUtil.Cache.LOCAL // CloudType.CRYPTO -> ... else -> { // it should be impossible to enter here, a cloud file could not be another type... @@ -368,27 +368,20 @@ abstract class CryptoImplDecorator( val ciphertextFile = cryptoFile.cloudFile val diskCache = getLruCacheFor(cryptoFile.cloudFile.cloud!!.type()!!) - val cacheKey = ciphertextFile.path.hashCode().toString().substring(3) // TODO: fare la stessa cacheKey nella list + val cacheKey = generateCacheKey(ciphertextFile) - var genThumbnail = false - if( sharedPreferencesHandler.useLruCache() && - !sharedPreferencesHandler.generateThumbnails().equals("Never") && // TODO: externalize string - diskCache != null && // - isImageMediaType(cryptoFile.name) - ) { - genThumbnail = true - } + val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) // TODO: solo se e' un file immagine!!! - val thumbnailTmp : File + val decryptedTempFile : File try { // cloudContentRepository.read(file, encryptedTmpFile, encryptedData, ...) // file appena letto dalla rete, portato in cache ancora cifrato! val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) - thumbnailTmp = File.createTempFile(encryptedTmpFile.nameWithoutExtension, ".tmp", internalCache) + decryptedTempFile = File.createTempFile(encryptedTmpFile.nameWithoutExtension, ".tmp", internalCache) - val thumbnailTmpOutputStream = thumbnailTmp.outputStream() + val decryptedTempFileOutputStream = decryptedTempFile.outputStream() progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) try { Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> @@ -400,7 +393,7 @@ abstract class CryptoImplDecorator( while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { buff.flip() data.write(buff.array(), 0, buff.remaining()) - thumbnailTmpOutputStream.write(buff.array(), 0, buff.remaining()) + decryptedTempFileOutputStream.write(buff.array(), 0, buff.remaining()) decrypted += read.toLong() progressAware @@ -414,8 +407,8 @@ abstract class CryptoImplDecorator( } } } finally { - thumbnailTmpOutputStream.flush() - thumbnailTmpOutputStream.close() + decryptedTempFileOutputStream.flush() + decryptedTempFileOutputStream.close() encryptedTmpFile.delete() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } @@ -424,32 +417,45 @@ abstract class CryptoImplDecorator( } // store it in cloud-related LRU cache - val thumbnailFile : File if(genThumbnail) { + generateAndStoreThumbNail(diskCache, cacheKey, decryptedTempFile) + } + decryptedTempFile.delete() + } - // generate the Bitmap (in memory) - val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) - } else { - ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) - } + protected fun generateCacheKey(cloudFile: CloudFile) : String{ + return cloudFile.path.hashCode().toString().substring(3) + } - // write the thumbnail in a file (on disk) - thumbnailFile = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) + private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { + return sharedPreferencesHandler.useLruCache() && + !sharedPreferencesHandler.generateThumbnails().equals("Never") && // TODO: externalize string + cache != null && // + isImageMediaType(fileName) + } - try { - diskCache?.let { - // store File to LruCache (on disk) - LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile) - } ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache") - } catch (e: IOException) { - Timber.tag("CryptoImplDecorator").e(e, "Failed to write downloaded file in LRU cache") - } + private fun generateAndStoreThumbNail(cache: DiskLruCache?, cacheKey: String, thumbnailTmp: File){ + // generate the Bitmap (in memory) + val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) + } else { + ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) + } + + // write the thumbnail in a file (on disk) + val thumbnailFile : File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) - thumbnailFile.delete() + try { + cache?.let { + // store File to LruCache (on disk) + LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile) + } ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache") + } catch (e: IOException) { + Timber.tag("CryptoImplDecorator").e(e, "Failed to write downloaded file in LRU cache") } - thumbnailTmp.delete() + + thumbnailFile.delete() } protected fun isImageMediaType(filename: String): Boolean { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index e896e2b44..09ba31001 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -170,7 +170,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { // if present, associate cached-thumbnail to the Cryptofile if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { - val cacheKey = cryptoNode.cloudFile.path.hashCode().toString().substring(3) + val cacheKey = generateCacheKey(cryptoNode.cloudFile) val diskCache = super.getLruCacheFor(cryptoNode.cloudFile.cloud!!.type()!!) diskCache?.let { @@ -467,7 +467,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { // Delete thumbnail file from cache val diskCache = super.getLruCacheFor(node.cloudFile.cloud!!.type()!!) - val cacheKey = node.cloudFile.path.hashCode().toString().substring(3) + val cacheKey = generateCacheKey(node.cloudFile) diskCache?.let { val cacheFile = it[cacheKey] diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt index b3d2fbee3..d92c6c275 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -20,7 +20,7 @@ class LruFileCacheUtil(context: Context) { private val parent: File = context.cacheDir enum class Cache { - DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE + DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE, LOCAL } fun resolve(cache: Cache?): File { @@ -31,6 +31,7 @@ class LruFileCacheUtil(context: Context) { Cache.S3 -> File(parent, "LruCacheS3") Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive") Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive") + Cache.LOCAL -> File(parent, "LruChaceLocal") else -> throw IllegalStateException() } } From b30afb12e84192454f6ce24406073d3801d5edd0 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 21 Apr 2024 22:26:49 +0200 Subject: [PATCH 07/29] Removed unused imports + fix typo --- .../presentation/ui/adapter/BrowseFilesAdapter.kt | 8 -------- .../java/org/cryptomator/util/file/LruFileCacheUtil.kt | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index d7ab194eb..0618088fa 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -1,11 +1,6 @@ package org.cryptomator.presentation.ui.adapter -import android.content.ContentResolver -import android.content.Context -import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.media.ThumbnailUtils -import android.os.Build import android.os.PatternMatcher import android.view.LayoutInflater import android.util.Size @@ -37,11 +32,8 @@ import org.cryptomator.presentation.util.FileSizeHelper import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.ResourceHelper.Companion.getDrawable import org.cryptomator.util.SharedPreferencesHandler -import org.cryptomator.util.file.LruFileCacheUtil import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypes -import java.io.File -import java.io.IOException import javax.inject.Inject class BrowseFilesAdapter @Inject diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt index d92c6c275..301a82264 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -31,7 +31,7 @@ class LruFileCacheUtil(context: Context) { Cache.S3 -> File(parent, "LruCacheS3") Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive") Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive") - Cache.LOCAL -> File(parent, "LruChaceLocal") + Cache.LOCAL -> File(parent, "LruCacheLocal") else -> throw IllegalStateException() } } From e0fa1be89bbde7abab646b585f5c684460bc3d60 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 17:44:23 +0200 Subject: [PATCH 08/29] changed names for retrieving cloud-related DiskLruCache --- .../data/cloud/crypto/CryptoImplDecorator.kt | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 1d3811dac..b83788874 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -70,26 +70,21 @@ abstract class CryptoImplDecorator( private val mimeTypes = MimeTypes(MimeTypeMap()) protected fun getLruCacheFor(type : CloudType): DiskLruCache? { - return getOrCreateLruCache(sharedPreferencesHandler.lruCacheSize(), dispatchCloud(type)!!) // unwrap should be safe! + return getOrCreateLruCache(getCacheTypeFromCloudType(type), sharedPreferencesHandler.lruCacheSize()) } - private fun getOrCreateLruCache(cacheSize: Int, key : LruFileCacheUtil.Cache): DiskLruCache? { - if(diskLruCache[key] == null) { - diskLruCache[key] = createLruCache(LruFileCacheUtil(context).resolve(key), cacheSize.toLong()) - } - return diskLruCache[key] - } - - private fun createLruCache(where: File, size: Long): DiskLruCache? { - return try { - DiskLruCache.create(where, size) - } catch (e: IOException) { - Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name") - null + private fun getOrCreateLruCache(key : LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { + return diskLruCache.computeIfAbsent(key) { + val where = LruFileCacheUtil(context).resolve(it) + try { + DiskLruCache.create(where, cacheSize.toLong()) + } catch (e: IOException) { + Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name") + null + } } } - - private fun dispatchCloud(type : CloudType) : LruFileCacheUtil.Cache? { + private fun getCacheTypeFromCloudType(type : CloudType) : LruFileCacheUtil.Cache { return when (type) { CloudType.DROPBOX -> LruFileCacheUtil.Cache.DROPBOX CloudType.GOOGLE_DRIVE -> LruFileCacheUtil.Cache.GOOGLE_DRIVE @@ -98,12 +93,7 @@ abstract class CryptoImplDecorator( CloudType.WEBDAV -> LruFileCacheUtil.Cache.WEBDAV CloudType.S3 -> LruFileCacheUtil.Cache.S3 CloudType.LOCAL -> LruFileCacheUtil.Cache.LOCAL - // CloudType.CRYPTO -> ... - else -> { - // it should be impossible to enter here, a cloud file could not be another type... - Timber.tag("CryptoImplDecorator").e("Unable to choose which cloud-cache") - null - } + else -> throw IllegalStateException() } } From b31de8e5de4d43eaceaff1aa76daabd1a4a4a1a4 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 18:36:10 +0200 Subject: [PATCH 09/29] modified the cachekey for DiskLruCache Also using cloud.id to be able to distinguish between two files with same path but on different instances of the same cloud --- .../org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index b83788874..c97f9cfa7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -414,7 +414,10 @@ abstract class CryptoImplDecorator( } protected fun generateCacheKey(cloudFile: CloudFile) : String{ - return cloudFile.path.hashCode().toString().substring(3) + var cacheKey = "" + cloudFile.cloud?.id()?.let { cacheKey += it } // distinguish between two files with same path but on different instances of the same cloud + cloudFile.path.hashCode().toString().let{ cacheKey += it } + return cacheKey } private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { From e20d516a0e3e58cb154876c55fe3dcc3e9c393c4 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 18:43:46 +0200 Subject: [PATCH 10/29] Added also in FormatPre7 the fetch of the thumbnail (not tested yet) --- .../data/cloud/crypto/CryptoImplVaultFormat7.kt | 12 +++++++----- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 09ba31001..366680376 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -172,11 +172,13 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) - val diskCache = super.getLruCacheFor(cryptoNode.cloudFile.cloud!!.type()!!) - diskCache?.let { - val cacheFile = it[cacheKey] - if (cacheFile != null) { - cryptoNode.thumbnail = cacheFile + cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> + val diskCache = super.getLruCacheFor(cloudType) + diskCache?.let { + val cacheFile = it[cacheKey] + if (cacheFile != null) { + cryptoNode.thumbnail = cacheFile + } } } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index a750bf6e1..e22bfe4f1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -128,6 +128,22 @@ internal class CryptoImplVaultFormatPre7( .filterIsInstance() .map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) + }.map { cryptoNode -> + // if present, associate cached-thumbnail to the Cryptofile + if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { + val cacheKey = generateCacheKey(cryptoNode.cloudFile) + + cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> + val diskCache = super.getLruCacheFor(cloudType) + diskCache?.let { + val cacheFile = it[cacheKey] + if (cacheFile != null) { + cryptoNode.thumbnail = cacheFile + } + } + } + } + cryptoNode } .toList() .filterNotNull() From ca57efb952dba2447274e19580b85ff4c936b495 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Mon, 29 Apr 2024 18:19:14 +0200 Subject: [PATCH 11/29] Add enum Thumbnail option --- .../data/cloud/crypto/CryptoImplDecorator.kt | 4 ++-- .../cryptomator/util/SharedPreferencesHandler.kt | 13 +++++++------ .../java/org/cryptomator/util/ThumbnailsOption.kt | 7 +++++++ 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index c97f9cfa7..1b4b743e1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -32,6 +32,7 @@ import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.ThumbnailsOption import org.cryptomator.util.file.LruFileCacheUtil import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypeMap @@ -362,7 +363,6 @@ abstract class CryptoImplDecorator( val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) - // TODO: solo se e' un file immagine!!! val decryptedTempFile : File try { @@ -422,7 +422,7 @@ abstract class CryptoImplDecorator( private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { return sharedPreferencesHandler.useLruCache() && - !sharedPreferencesHandler.generateThumbnails().equals("Never") && // TODO: externalize string + sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER && cache != null && // isImageMediaType(fileName) } diff --git a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt index 4b5121222..8eb93f180 100644 --- a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt +++ b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt @@ -161,12 +161,13 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen return defaultSharedPreferences.getValue(PHOTO_UPLOAD_INCLUDING_VIDEOS, false) } - fun generateThumbnails() { - defaultSharedPreferences.getValue(THUMBNAIL_GENERATION, "Never") - } - - fun generateThumbnails(modality: String) { - defaultSharedPreferences.setValue(THUMBNAIL_GENERATION, modality) + fun generateThumbnails(): ThumbnailsOption { + return when(defaultSharedPreferences.getValue(THUMBNAIL_GENERATION, "NEVER")){ + "NEVER" -> ThumbnailsOption.NEVER + "PER_FILE" -> ThumbnailsOption.PER_FILE + "PER_FOLDER" -> ThumbnailsOption.PER_FOLDER + else -> ThumbnailsOption.NEVER + } } fun useLruCache(): Boolean { diff --git a/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt b/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt new file mode 100644 index 000000000..91fdc623b --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt @@ -0,0 +1,7 @@ +package org.cryptomator.util + +enum class ThumbnailsOption { + NEVER, + PER_FILE, + PER_FOLDER +} \ No newline at end of file From 041b2d8697f32cdf72be6f4ec6a2d58c0c38d651 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Mon, 29 Apr 2024 22:08:45 +0200 Subject: [PATCH 12/29] minor changes --- .../data/cloud/crypto/CryptoImplDecorator.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 1b4b743e1..086759a9f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -358,16 +358,11 @@ abstract class CryptoImplDecorator( fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { val ciphertextFile = cryptoFile.cloudFile - val diskCache = getLruCacheFor(cryptoFile.cloudFile.cloud!!.type()!!) + val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) - val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) - val decryptedTempFile : File try { - - // cloudContentRepository.read(file, encryptedTmpFile, encryptedData, ...) - // file appena letto dalla rete, portato in cache ancora cifrato! val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) decryptedTempFile = File.createTempFile(encryptedTmpFile.nameWithoutExtension, ".tmp", internalCache) @@ -408,7 +403,7 @@ abstract class CryptoImplDecorator( // store it in cloud-related LRU cache if(genThumbnail) { - generateAndStoreThumbNail(diskCache, cacheKey, decryptedTempFile) + generateAndStoreThumbnail(diskCache, cacheKey, decryptedTempFile) } decryptedTempFile.delete() } @@ -423,11 +418,11 @@ abstract class CryptoImplDecorator( private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER && - cache != null && // + cache != null && isImageMediaType(fileName) } - private fun generateAndStoreThumbNail(cache: DiskLruCache?, cacheKey: String, thumbnailTmp: File){ + private fun generateAndStoreThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailTmp: File) { // generate the Bitmap (in memory) val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) @@ -445,7 +440,7 @@ abstract class CryptoImplDecorator( LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile) } ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache") } catch (e: IOException) { - Timber.tag("CryptoImplDecorator").e(e, "Failed to write downloaded file in LRU cache") + Timber.tag("CryptoImplDecorator").e(e, "Failed to write the thumbnail in DiskLruCache") } thumbnailFile.delete() From abcc3e2193970604a8a8597e5dae5de92c233a99 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 8 May 2024 00:41:53 +0200 Subject: [PATCH 13/29] Change cachekey for thumbnails --- .../data/cloud/crypto/CryptoImplDecorator.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 086759a9f..bd313a7f2 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -409,10 +409,17 @@ abstract class CryptoImplDecorator( } protected fun generateCacheKey(cloudFile: CloudFile) : String{ - var cacheKey = "" - cloudFile.cloud?.id()?.let { cacheKey += it } // distinguish between two files with same path but on different instances of the same cloud - cloudFile.path.hashCode().toString().let{ cacheKey += it } - return cacheKey + return buildString { + // distinguish between two files with same path but on different instances of the same cloud + if (cloudFile.cloud?.id() != null) + this.append(cloudFile.cloud!!.id()) + else + // this.append(null obj) will add the string "null" + this.append("c") // "common" + this.append("-") + + this.append(cloudFile.path.hashCode()) + } } private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { From d02e811a1442bab407b43dac7776866a8365d6df Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 8 May 2024 01:05:56 +0200 Subject: [PATCH 14/29] Use onEach and added the delete operation in the FormatPre7 --- .../data/cloud/crypto/CryptoImplDecorator.kt | 1 - .../cloud/crypto/CryptoImplVaultFormat7.kt | 23 ++++++++----------- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 21 +++++++++++------ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index bd313a7f2..6fd1d6705 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -417,7 +417,6 @@ abstract class CryptoImplDecorator( // this.append(null obj) will add the string "null" this.append("c") // "common" this.append("-") - this.append(cloudFile.path.hashCode()) } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 366680376..0b53cf03a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -166,23 +166,19 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } }.map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.map { cryptoNode -> - + }.onEach { cryptoNode -> // if present, associate cached-thumbnail to the Cryptofile - if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { + if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) - cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> - val diskCache = super.getLruCacheFor(cloudType) - diskCache?.let { - val cacheFile = it[cacheKey] + getLruCacheFor(cloudType)?.let { diskCache -> + val cacheFile = diskCache[cacheKey] if (cacheFile != null) { cryptoNode.thumbnail = cacheFile } } } } - cryptoNode }.toList().filterNotNull() } @@ -468,13 +464,12 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } // Delete thumbnail file from cache - val diskCache = super.getLruCacheFor(node.cloudFile.cloud!!.type()!!) val cacheKey = generateCacheKey(node.cloudFile) - - diskCache?.let { - val cacheFile = it[cacheKey] - if (cacheFile != null) { - diskCache.delete(cacheKey) + node.cloudFile.cloud?.type()?.let { cloudType -> + getLruCacheFor(cloudType)?.let { diskCache -> + if (diskCache[cacheKey] != null) { + diskCache.delete(cacheKey) + } } } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index e22bfe4f1..a00933d11 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -128,22 +128,19 @@ internal class CryptoImplVaultFormatPre7( .filterIsInstance() .map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.map { cryptoNode -> + }.onEach { cryptoNode -> // if present, associate cached-thumbnail to the Cryptofile - if(cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { + if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) - cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> - val diskCache = super.getLruCacheFor(cloudType) - diskCache?.let { - val cacheFile = it[cacheKey] + getLruCacheFor(cloudType)?.let { diskCache -> + val cacheFile = diskCache[cacheKey] if (cacheFile != null) { cryptoNode.thumbnail = cacheFile } } } } - cryptoNode } .toList() .filterNotNull() @@ -264,6 +261,16 @@ internal class CryptoImplVaultFormatPre7( evictFromCache(node) } else if (node is CryptoFile) { cloudContentRepository.delete(node.cloudFile) + + // Delete thumbnail file from cache + val cacheKey = generateCacheKey(node.cloudFile) + node.cloudFile.cloud?.type()?.let{ cloudType -> + getLruCacheFor(cloudType)?.let { diskCache -> + if(diskCache[cacheKey] != null) { + diskCache.delete(cacheKey) + } + } + } } } From 6daefd7460f2e7719167a66b0c5577dae84ea23d Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Mon, 29 Apr 2024 00:37:19 +0200 Subject: [PATCH 15/29] Added a separate thread to acquire the bitmap of the image and generate the thumbnail --- .../data/cloud/crypto/CryptoImplDecorator.kt | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 6fd1d6705..244e1fa28 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -4,9 +4,8 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.ThumbnailUtils -import android.os.Build -import android.util.Size import com.tomclaw.cache.DiskLruCache +import okhttp3.internal.closeQuietly import org.cryptomator.cryptolib.api.Cryptor import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel @@ -50,6 +49,9 @@ import java.util.Queue import java.util.UUID import java.util.function.Supplier import timber.log.Timber +import java.io.PipedInputStream +import java.io.PipedOutputStream +import kotlin.concurrent.thread abstract class CryptoImplDecorator( @@ -361,12 +363,30 @@ abstract class CryptoImplDecorator( val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) - val decryptedTempFile : File + var thumbnailBitmap : Bitmap? = null + + val thumbnailWriter = PipedOutputStream() + val thumbnailReader = PipedInputStream(thumbnailWriter) + try { + // cloudContentRepository.read(file, encryptedTmpFile, encryptedData, ...) + // file appena letto dalla rete, portato in cache ancora cifrato! val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) - decryptedTempFile = File.createTempFile(encryptedTmpFile.nameWithoutExtension, ".tmp", internalCache) - val decryptedTempFileOutputStream = decryptedTempFile.outputStream() + // TODO: reusable thread? + // A thread pool is a managed collection of threads that runs tasks in parallel from a queue. + // https://developer.android.com/develop/background-work/background-tasks/asynchronous/java-threads + val t = thread(start = false, name = "S.AN-DRO") { // Simply A New Data Readable Output + try { + val bitmap = BitmapFactory.decodeStream(thumbnailReader) // wait for the full image + thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, 100, 100) + thumbnailReader.closeQuietly() + } catch (e : Exception) { + Timber.e("Bitmap generation crashed") + } + } + + t.start() progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) try { Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> @@ -378,34 +398,36 @@ abstract class CryptoImplDecorator( while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { buff.flip() data.write(buff.array(), 0, buff.remaining()) - decryptedTempFileOutputStream.write(buff.array(), 0, buff.remaining()) + thumbnailWriter.write(buff.array(), 0, buff.remaining()) decrypted += read.toLong() + progressAware - .onProgress( - Progress.progress(DownloadState.decryption(cryptoFile)) // - .between(0) // - .and(cleartextSize) // - .withValue(decrypted) - ) + .onProgress( + Progress.progress(DownloadState.decryption(cryptoFile)) // + .between(0) // + .and(cleartextSize) // + .withValue(decrypted) + ) } } + thumbnailWriter.flush() } } finally { - decryptedTempFileOutputStream.flush() - decryptedTempFileOutputStream.close() encryptedTmpFile.delete() + thumbnailWriter.closeQuietly() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } + t.join() // wait the thread + thumbnailReader.closeQuietly() } catch (e: IOException) { throw FatalBackendException(e) } // store it in cloud-related LRU cache - if(genThumbnail) { - generateAndStoreThumbnail(diskCache, cacheKey, decryptedTempFile) + if(genThumbnail && thumbnailBitmap != null) { + generateAndStoreThumbnail(diskCache, cacheKey, thumbnailBitmap!!) } - decryptedTempFile.delete() } protected fun generateCacheKey(cloudFile: CloudFile) : String{ @@ -428,17 +450,17 @@ abstract class CryptoImplDecorator( isImageMediaType(fileName) } - private fun generateAndStoreThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailTmp: File) { - // generate the Bitmap (in memory) - val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) - } else { - ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) - } + private fun generateAndStoreThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap){ +// // generate the Bitmap (in memory) +// val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +// ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) +// } else { +// ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) +// } // write the thumbnail in a file (on disk) val thumbnailFile : File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) + thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) try { cache?.let { From 4d0e715e68cc5e94653bda950eb74f5b1f4f9884 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 1 May 2024 12:32:45 +0200 Subject: [PATCH 16/29] Add thumbanil generator thread pool executor and subsample image stream --- .../data/cloud/crypto/CryptoImplDecorator.kt | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 244e1fa28..8fc152478 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -4,8 +4,8 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.ThumbnailUtils +import com.google.common.util.concurrent.ThreadFactoryBuilder import com.tomclaw.cache.DiskLruCache -import okhttp3.internal.closeQuietly import org.cryptomator.cryptolib.api.Cryptor import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel @@ -37,6 +37,7 @@ import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypeMap import org.cryptomator.util.file.MimeTypes import java.io.ByteArrayOutputStream +import java.io.Closeable import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -51,7 +52,10 @@ import java.util.function.Supplier import timber.log.Timber import java.io.PipedInputStream import java.io.PipedOutputStream -import kotlin.concurrent.thread +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future +import kotlin.math.ceil abstract class CryptoImplDecorator( @@ -72,6 +76,11 @@ abstract class CryptoImplDecorator( private val mimeTypes = MimeTypes(MimeTypeMap()) + private val thumbnailExecutorService: ExecutorService by lazy { + val threadFactory = ThreadFactoryBuilder().setNameFormat("thumbnail-generation-thread-%d").build() + Executors.newFixedThreadPool(3, threadFactory) + } + protected fun getLruCacheFor(type : CloudType): DiskLruCache? { return getOrCreateLruCache(getCacheTypeFromCloudType(type), sharedPreferencesHandler.lruCacheSize()) } @@ -363,30 +372,17 @@ abstract class CryptoImplDecorator( val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) - var thumbnailBitmap : Bitmap? = null val thumbnailWriter = PipedOutputStream() val thumbnailReader = PipedInputStream(thumbnailWriter) try { - // cloudContentRepository.read(file, encryptedTmpFile, encryptedData, ...) - // file appena letto dalla rete, portato in cache ancora cifrato! val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) - // TODO: reusable thread? - // A thread pool is a managed collection of threads that runs tasks in parallel from a queue. - // https://developer.android.com/develop/background-work/background-tasks/asynchronous/java-threads - val t = thread(start = false, name = "S.AN-DRO") { // Simply A New Data Readable Output - try { - val bitmap = BitmapFactory.decodeStream(thumbnailReader) // wait for the full image - thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, 100, 100) - thumbnailReader.closeQuietly() - } catch (e : Exception) { - Timber.e("Bitmap generation crashed") - } + if (genThumbnail) { + startThumbnailGeneratorThread(diskCache, cacheKey, thumbnailReader) } - t.start() progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) try { Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> @@ -398,7 +394,9 @@ abstract class CryptoImplDecorator( while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { buff.flip() data.write(buff.array(), 0, buff.remaining()) - thumbnailWriter.write(buff.array(), 0, buff.remaining()) + if (genThumbnail) { + thumbnailWriter.write(buff.array(), 0, buff.remaining()) + } decrypted += read.toLong() @@ -412,21 +410,64 @@ abstract class CryptoImplDecorator( } } thumbnailWriter.flush() + closeQuietly(thumbnailWriter) } } finally { encryptedTmpFile.delete() - thumbnailWriter.closeQuietly() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } - t.join() // wait the thread - thumbnailReader.closeQuietly() + + closeQuietly(thumbnailReader) } catch (e: IOException) { throw FatalBackendException(e) } + } - // store it in cloud-related LRU cache - if(genThumbnail && thumbnailBitmap != null) { - generateAndStoreThumbnail(diskCache, cacheKey, thumbnailBitmap!!) + private fun closeQuietly(closeable : Closeable) { + try { + closeable.close(); + } catch (e : IOException) { + // ignore + } + } + private fun startThumbnailGeneratorThread(diskCache: DiskLruCache?, cacheKey: String, thumbnailReader: PipedInputStream) : Future<*> { + return thumbnailExecutorService.submit { + try { + val options = BitmapFactory.Options() + val thumbnailBitmap : Bitmap? + // options.inJustDecodeBounds = true + // read properties of the image: outWidth, outHeight (no bitmap allocation!) + // BitmapFactory.decodeStream(thumbnailReaderTee, null, options) + // options.inJustDecodeBounds = false + // options.outWidth; options.outHeight + options.inSampleSize = 4 // pixel number reduced by a factor of 1/16 + // options.inSampleSize = 8 // pixel number reduced by a factor of 1/64 + + // obtain a subsampled version of the image + val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options) + + val thumbnailWidth = 100 + val thumbnailHeight = 100 +// var aspectRatio = 1f +// bitmap?.let { +// if (it.height != 0) { +// aspectRatio = it.width.toFloat() / it.height +// } +// } +// val thumbnailHeight = ceil(1 / aspectRatio * thumbnailWidth).toInt() + + // generate thumbnail preserving aspect ratio + thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight) + + // store it in cloud-related LRU cache + if(thumbnailBitmap != null) { + storeThumbnail(diskCache, cacheKey, thumbnailBitmap) + } + + closeQuietly(thumbnailReader) + } catch (e: Exception) { + Timber.e("Bitmap generation crashed") + } } } @@ -450,14 +491,7 @@ abstract class CryptoImplDecorator( isImageMediaType(fileName) } - private fun generateAndStoreThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap){ -// // generate the Bitmap (in memory) -// val bitmap : Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// ThumbnailUtils.createImageThumbnail(thumbnailTmp, Size(100, 100), null) -// } else { -// ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(thumbnailTmp.path), 100, 100) -// } - + private fun storeThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap){ // write the thumbnail in a file (on disk) val thumbnailFile : File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) From fe0151d38e3edcbd93fd01de5bd53338a7b1a6a1 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Wed, 8 May 2024 11:50:24 +0200 Subject: [PATCH 17/29] Cleanup --- .../data/cloud/crypto/CryptoImplDecorator.kt | 71 +++++++------------ .../cloud/crypto/CryptoImplVaultFormat7.kt | 3 - .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 6 +- 3 files changed, 27 insertions(+), 53 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 8fc152478..fbf4300e2 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -43,19 +43,18 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.OutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream import java.nio.ByteBuffer import java.nio.channels.Channels import java.util.LinkedList import java.util.Queue import java.util.UUID -import java.util.function.Supplier -import timber.log.Timber -import java.io.PipedInputStream -import java.io.PipedOutputStream import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future -import kotlin.math.ceil +import java.util.function.Supplier +import timber.log.Timber abstract class CryptoImplDecorator( @@ -81,10 +80,11 @@ abstract class CryptoImplDecorator( Executors.newFixedThreadPool(3, threadFactory) } - protected fun getLruCacheFor(type : CloudType): DiskLruCache? { + protected fun getLruCacheFor(type: CloudType): DiskLruCache? { return getOrCreateLruCache(getCacheTypeFromCloudType(type), sharedPreferencesHandler.lruCacheSize()) } - private fun getOrCreateLruCache(key : LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { + + private fun getOrCreateLruCache(key: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { return diskLruCache.computeIfAbsent(key) { val where = LruFileCacheUtil(context).resolve(it) try { @@ -96,7 +96,7 @@ abstract class CryptoImplDecorator( } } - private fun getCacheTypeFromCloudType(type : CloudType) : LruFileCacheUtil.Cache { + private fun getCacheTypeFromCloudType(type: CloudType): LruFileCacheUtil.Cache { return when (type) { CloudType.DROPBOX -> LruFileCacheUtil.Cache.DROPBOX CloudType.GOOGLE_DRIVE -> LruFileCacheUtil.Cache.GOOGLE_DRIVE @@ -401,12 +401,12 @@ abstract class CryptoImplDecorator( decrypted += read.toLong() progressAware - .onProgress( - Progress.progress(DownloadState.decryption(cryptoFile)) // - .between(0) // - .and(cleartextSize) // - .withValue(decrypted) - ) + .onProgress( + Progress.progress(DownloadState.decryption(cryptoFile)) // + .between(0) // + .and(cleartextSize) // + .withValue(decrypted) + ) } } thumbnailWriter.flush() @@ -423,44 +423,27 @@ abstract class CryptoImplDecorator( } } - private fun closeQuietly(closeable : Closeable) { + private fun closeQuietly(closeable: Closeable) { try { closeable.close(); - } catch (e : IOException) { + } catch (e: IOException) { // ignore } } - private fun startThumbnailGeneratorThread(diskCache: DiskLruCache?, cacheKey: String, thumbnailReader: PipedInputStream) : Future<*> { + + private fun startThumbnailGeneratorThread(diskCache: DiskLruCache?, cacheKey: String, thumbnailReader: PipedInputStream): Future<*> { return thumbnailExecutorService.submit { try { val options = BitmapFactory.Options() - val thumbnailBitmap : Bitmap? - // options.inJustDecodeBounds = true - // read properties of the image: outWidth, outHeight (no bitmap allocation!) - // BitmapFactory.decodeStream(thumbnailReaderTee, null, options) - // options.inJustDecodeBounds = false - // options.outWidth; options.outHeight + val thumbnailBitmap: Bitmap? options.inSampleSize = 4 // pixel number reduced by a factor of 1/16 - // options.inSampleSize = 8 // pixel number reduced by a factor of 1/64 - // obtain a subsampled version of the image val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options) - val thumbnailWidth = 100 val thumbnailHeight = 100 -// var aspectRatio = 1f -// bitmap?.let { -// if (it.height != 0) { -// aspectRatio = it.width.toFloat() / it.height -// } -// } -// val thumbnailHeight = ceil(1 / aspectRatio * thumbnailWidth).toInt() - - // generate thumbnail preserving aspect ratio thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight) - // store it in cloud-related LRU cache - if(thumbnailBitmap != null) { + if (thumbnailBitmap != null) { storeThumbnail(diskCache, cacheKey, thumbnailBitmap) } @@ -471,34 +454,30 @@ abstract class CryptoImplDecorator( } } - protected fun generateCacheKey(cloudFile: CloudFile) : String{ + protected fun generateCacheKey(cloudFile: CloudFile): String { return buildString { - // distinguish between two files with same path but on different instances of the same cloud if (cloudFile.cloud?.id() != null) this.append(cloudFile.cloud!!.id()) else - // this.append(null obj) will add the string "null" this.append("c") // "common" this.append("-") this.append(cloudFile.path.hashCode()) } } - private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String) : Boolean { - return sharedPreferencesHandler.useLruCache() && + private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String): Boolean { + return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER && cache != null && isImageMediaType(fileName) } - private fun storeThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap){ - // write the thumbnail in a file (on disk) - val thumbnailFile : File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) + private fun storeThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap) { + val thumbnailFile: File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache) thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream()) try { cache?.let { - // store File to LruCache (on disk) LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile) } ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache") } catch (e: IOException) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 0b53cf03a..02fcb2aeb 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -87,7 +87,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { val shortFileName = BaseEncoding.base64Url().encode(hash) + LONG_NODE_FILE_EXT var dirFolder = cloudContentRepository.folder(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, shortFileName) - // if folder already exists in case of renaming if (!cloudContentRepository.exists(dirFolder)) { dirFolder = cloudContentRepository.create(dirFolder) } @@ -167,7 +166,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { }.map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) }.onEach { cryptoNode -> - // if present, associate cached-thumbnail to the Cryptofile if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> @@ -463,7 +461,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { cloudContentRepository.delete(node.cloudFile) } - // Delete thumbnail file from cache val cacheKey = generateCacheKey(node.cloudFile) node.cloudFile.cloud?.type()?.let { cloudType -> getLruCacheFor(cloudType)?.let { diskCache -> diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index a00933d11..882507480 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -129,7 +129,6 @@ internal class CryptoImplVaultFormatPre7( .map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) }.onEach { cryptoNode -> - // if present, associate cached-thumbnail to the Cryptofile if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> @@ -262,11 +261,10 @@ internal class CryptoImplVaultFormatPre7( } else if (node is CryptoFile) { cloudContentRepository.delete(node.cloudFile) - // Delete thumbnail file from cache val cacheKey = generateCacheKey(node.cloudFile) - node.cloudFile.cloud?.type()?.let{ cloudType -> + node.cloudFile.cloud?.type()?.let { cloudType -> getLruCacheFor(cloudType)?.let { diskCache -> - if(diskCache[cacheKey] != null) { + if (diskCache[cacheKey] != null) { diskCache.delete(cacheKey) } } From 00f766fda5aa8d4fbc95288d8c371114fe031b6e Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Wed, 8 May 2024 14:48:24 +0200 Subject: [PATCH 18/29] Manage rename and move files in cache --- .../data/cloud/crypto/CryptoImplDecorator.kt | 12 ++++++++++++ .../data/cloud/crypto/CryptoImplVaultFormat7.kt | 1 + .../data/cloud/crypto/CryptoImplVaultFormatPre7.kt | 1 + 3 files changed, 14 insertions(+) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index fbf4300e2..423a46dc4 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -95,6 +95,18 @@ abstract class CryptoImplDecorator( } } } + protected fun renameFileInCache(source: CryptoFile, target: CryptoFile){ + val oldCacheKey = generateCacheKey(source.cloudFile) + val newCacheKey = generateCacheKey(target.cloudFile) + source.cloudFile.cloud?.type()?.let { cloudType -> + getLruCacheFor(cloudType)?.let { diskCache -> + if (diskCache[oldCacheKey] != null) { + target.thumbnail = diskCache.put(newCacheKey, diskCache[oldCacheKey]) + diskCache.delete(oldCacheKey) + } + } + } + } private fun getCacheTypeFromCloudType(type: CloudType): LruFileCacheUtil.Cache { return when (type) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 02fcb2aeb..8214d6b73 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -391,6 +391,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { @Throws(BackendException::class) override fun move(source: CryptoFile, target: CryptoFile): CryptoFile { + renameFileInCache(source, target) return if (source.cloudFile.parent.name.endsWith(LONG_NODE_FILE_EXT)) { val targetDirFolder = cloudContentRepository.folder(target.cloudFile.parent, target.cloudFile.name) val cryptoFile: CryptoFile = if (target.cloudFile.name.endsWith(LONG_NODE_FILE_EXT)) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index 882507480..d103ff720 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -240,6 +240,7 @@ internal class CryptoImplVaultFormatPre7( @Throws(BackendException::class) override fun move(source: CryptoFile, target: CryptoFile): CryptoFile { assertCryptoFileAlreadyExists(target) + renameFileInCache(source, target) return file(target, cloudContentRepository.move(source.cloudFile, target.cloudFile), source.size) } From 68b87448474cb38a5bd91e9656db331d264580ad Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Fri, 6 Sep 2024 17:46:05 +0200 Subject: [PATCH 19/29] Rebase and PR minor changes --- .../data/cloud/crypto/CryptoImplDecorator.kt | 22 +++++++------------ .../presentation/model/CloudNodeModel.kt | 2 -- .../presenter/BrowseFilesPresenter.kt | 2 -- .../presenter/VaultListPresenter.kt | 2 +- .../ui/adapter/BrowseFilesAdapter.kt | 7 ++---- .../ui/bottomsheet/FileSettingsBottomSheet.kt | 11 ++++------ .../ui/fragment/BrowseFilesFragment.kt | 1 - .../ui/fragment/SettingsFragment.kt | 1 - 8 files changed, 15 insertions(+), 33 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 423a46dc4..6b46325f4 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -84,18 +84,19 @@ abstract class CryptoImplDecorator( return getOrCreateLruCache(getCacheTypeFromCloudType(type), sharedPreferencesHandler.lruCacheSize()) } - private fun getOrCreateLruCache(key: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { - return diskLruCache.computeIfAbsent(key) { - val where = LruFileCacheUtil(context).resolve(it) + private fun getOrCreateLruCache(cache: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { + return diskLruCache.computeIfAbsent(cache) { + val cacheFile = LruFileCacheUtil(context).resolve(it) try { - DiskLruCache.create(where, cacheSize.toLong()) + DiskLruCache.create(cacheFile, cacheSize.toLong()) } catch (e: IOException) { - Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name") + Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $cacheFile.name") null } } } - protected fun renameFileInCache(source: CryptoFile, target: CryptoFile){ + + protected fun renameFileInCache(source: CryptoFile, target: CryptoFile) { val oldCacheKey = generateCacheKey(source.cloudFile) val newCacheKey = generateCacheKey(target.cloudFile) source.cloudFile.cloud?.type()?.let { cloudType -> @@ -467,14 +468,7 @@ abstract class CryptoImplDecorator( } protected fun generateCacheKey(cloudFile: CloudFile): String { - return buildString { - if (cloudFile.cloud?.id() != null) - this.append(cloudFile.cloud!!.id()) - else - this.append("c") // "common" - this.append("-") - this.append(cloudFile.path.hashCode()) - } + return String.format("%s-%d", cloudFile.cloud?.id() ?: "common", cloudFile.path.hashCode()) } private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String): Boolean { diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt index 05957766c..4e07dd4af 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt @@ -1,6 +1,5 @@ package org.cryptomator.presentation.model -import android.graphics.Bitmap import org.cryptomator.domain.CloudNode import java.io.Serializable @@ -9,7 +8,6 @@ abstract class CloudNodeModel internal constructor(private val cl var oldName: String? = null var progress: ProgressModel? = null var isSelected = false - val name: String get() = cloudNode.name val simpleName: String diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index 9f2cd00cf..fa9e235f6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -2,7 +2,6 @@ package org.cryptomator.presentation.presenter import android.content.ActivityNotFoundException import android.content.Intent -import android.graphics.BitmapFactory import android.net.Uri import android.provider.DocumentsContract import android.widget.Toast @@ -514,7 +513,6 @@ class BrowseFilesPresenter @Inject constructor( // ) } else if (!lowerFileName.endsWith(".gif") && isImageMediaType(cloudFile.name)) { val cloudFileNodes = previewCloudFileNodes - val imagePreviewStore = ImagePreviewFilesStore( // cloudFileNodes, // cloudFileNodes.indexOf(cloudFile) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 6750b1241..9a6285241 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -123,7 +123,7 @@ class VaultListPresenter @Inject constructor( // sharedPreferencesHandler.vaultsRemovedDuringMigration(null) } - checkLicense() + // checkLicense() checkPermissions() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 0618088fa..32f1fe1dc 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -3,7 +3,6 @@ package org.cryptomator.presentation.ui.adapter import android.graphics.BitmapFactory import android.os.PatternMatcher import android.view.LayoutInflater -import android.util.Size import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -140,13 +139,11 @@ constructor( } private fun bindNodeImage(node: CloudNodeModel<*>) { - binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) - if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail != null) { val bitmap = BitmapFactory.decodeFile(node.thumbnail!!.absolutePath) - itemView.cloudNodeImage.setImageBitmap(bitmap) + binding.cloudNodeImage.setImageBitmap(bitmap) } else { - itemView.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) + binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt index 8b573e8ac..e2a0a87c0 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt @@ -26,18 +26,15 @@ class FileSettingsBottomSheet : BaseBottomSheet Date: Sat, 7 Sep 2024 16:55:54 +0200 Subject: [PATCH 20/29] Refactor thumbnail association --- .../data/cloud/crypto/CryptoImplDecorator.kt | 33 +++++++++++++++---- .../cloud/crypto/CryptoImplVaultFormat7.kt | 14 ++------ .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 18 ++-------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 6b46325f4..80f01abc8 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -384,7 +384,7 @@ abstract class CryptoImplDecorator( val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) - val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) + val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) val thumbnailWriter = PipedOutputStream() val thumbnailReader = PipedInputStream(thumbnailWriter) @@ -471,11 +471,30 @@ abstract class CryptoImplDecorator( return String.format("%s-%d", cloudFile.cloud?.id() ?: "common", cloudFile.path.hashCode()) } - private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String): Boolean { - return sharedPreferencesHandler.useLruCache() && - sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER && - cache != null && - isImageMediaType(fileName) + private fun isThumbnailGenerationAvailable(cache: DiskLruCache?, fileName: String): Boolean { + return isGenerateThumbnailsEnabled() && cache != null && isImageMediaType(fileName) + } + + protected fun associateThumbnailIfInCache(list: List): List { + if (isGenerateThumbnailsEnabled()) { + val firstCryptoFile = list.find { it is CryptoFile } ?: return list + val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list + val diskCache = getLruCacheFor(cloudType) ?: return list + list.onEach { cryptoNode -> + if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { + val cacheKey = generateCacheKey(cryptoNode.cloudFile) + val cacheFile = diskCache[cacheKey] + if (cacheFile != null) { + cryptoNode.thumbnail = cacheFile + } + } + } + } + return list + } + + private fun isGenerateThumbnailsEnabled(): Boolean { + return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER } private fun storeThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap) { @@ -493,7 +512,7 @@ abstract class CryptoImplDecorator( thumbnailFile.delete() } - protected fun isImageMediaType(filename: String): Boolean { + private fun isImageMediaType(filename: String): Boolean { return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 8214d6b73..c6fa3bdab 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -165,18 +165,8 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } }.map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.onEach { cryptoNode -> - if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { - val cacheKey = generateCacheKey(cryptoNode.cloudFile) - cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> - getLruCacheFor(cloudType)?.let { diskCache -> - val cacheFile = diskCache[cacheKey] - if (cacheFile != null) { - cryptoNode.thumbnail = cacheFile - } - } - } - } + }.also { + associateThumbnailIfInCache(it) }.toList().filterNotNull() } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index d103ff720..a4b7328c8 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -128,21 +128,9 @@ internal class CryptoImplVaultFormatPre7( .filterIsInstance() .map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.onEach { cryptoNode -> - if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { - val cacheKey = generateCacheKey(cryptoNode.cloudFile) - cryptoNode.cloudFile.cloud?.type()?.let { cloudType -> - getLruCacheFor(cloudType)?.let { diskCache -> - val cacheFile = diskCache[cacheKey] - if (cacheFile != null) { - cryptoNode.thumbnail = cacheFile - } - } - } - } - } - .toList() - .filterNotNull() + }.also { + associateThumbnailIfInCache(it) + }.toList().filterNotNull() } @Throws(BackendException::class) From e05f206fddd0e8efd8085b4e0e80e2373105fd40 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Mon, 9 Sep 2024 18:31:50 +0200 Subject: [PATCH 21/29] Force thumbnail generation to compare listing performance --- .../cryptomator/data/cloud/crypto/CryptoImplDecorator.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 80f01abc8..54eecc99a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -480,12 +480,18 @@ abstract class CryptoImplDecorator( val firstCryptoFile = list.find { it is CryptoFile } ?: return list val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list val diskCache = getLruCacheFor(cloudType) ?: return list - list.onEach { cryptoNode -> + list.forEach { cryptoNode -> if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { val cacheKey = generateCacheKey(cryptoNode.cloudFile) val cacheFile = diskCache[cacheKey] if (cacheFile != null) { cryptoNode.thumbnail = cacheFile + } else { + // TODO + // force thumbnail generation (~PER FOLDER) + val trash = File.createTempFile(cryptoNode.name, ".temp", internalCache) + read(cryptoNode, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + trash.delete() } } } From 64ba7cfd487da0cb887558c9398a5fc0e447202a Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Tue, 10 Sep 2024 07:55:22 +0200 Subject: [PATCH 22/29] Fix mistake Sorry, clearly it was an oversight! --- .../cryptomator/presentation/presenter/VaultListPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 9a6285241..6750b1241 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -123,7 +123,7 @@ class VaultListPresenter @Inject constructor( // sharedPreferencesHandler.vaultsRemovedDuringMigration(null) } - // checkLicense() + checkLicense() checkPermissions() } From c25714d36a12eca5a22429bb9779f27610fd40a7 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Tue, 24 Sep 2024 16:15:58 +0200 Subject: [PATCH 23/29] Add multithreaded parallelism in the thumbnail generation Multithreaded association and generation of thumbnails for a pseudo PER FOLDER option --- .../data/cloud/crypto/CryptoImplDecorator.kt | 118 +++++++++++++++--- 1 file changed, 103 insertions(+), 15 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 54eecc99a..dc7864d08 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -50,6 +50,9 @@ import java.nio.channels.Channels import java.util.LinkedList import java.util.Queue import java.util.UUID +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorCompletionService import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future @@ -77,7 +80,7 @@ abstract class CryptoImplDecorator( private val thumbnailExecutorService: ExecutorService by lazy { val threadFactory = ThreadFactoryBuilder().setNameFormat("thumbnail-generation-thread-%d").build() - Executors.newFixedThreadPool(3, threadFactory) + Executors.newCachedThreadPool(threadFactory) } protected fun getLruCacheFor(type: CloudType): DiskLruCache? { @@ -378,6 +381,69 @@ abstract class CryptoImplDecorator( } else name.substring(lastDot + 1) } + @Throws(BackendException::class) + fun readGenerateThumbnail(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware): Future<*> { + // TODO refactor this method with the real read + + val ciphertextFile = cryptoFile.cloudFile + var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null) + + val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } + val cacheKey = generateCacheKey(ciphertextFile) + val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) + + val thumbnailWriter = PipedOutputStream() + val thumbnailReader = PipedInputStream(thumbnailWriter) + + try { + val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) + + if (genThumbnail) { + futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader) + } + + progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) + try { + Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> + DecryptingReadableByteChannel(readableByteChannel, cryptor(), true).use { decryptingReadableByteChannel -> + val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize()) + val cleartextSize = cryptoFile.size ?: Long.MAX_VALUE + var decrypted: Long = 0 + var read: Int + while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { + buff.flip() + data.write(buff.array(), 0, buff.remaining()) + if (genThumbnail) { + thumbnailWriter.write(buff.array(), 0, buff.remaining()) + } + + decrypted += read.toLong() + + progressAware + .onProgress( + Progress.progress(DownloadState.decryption(cryptoFile)) // + .between(0) // + .and(cleartextSize) // + .withValue(decrypted) + ) + } + } + thumbnailWriter.flush() + closeQuietly(thumbnailWriter) + } + } finally { + encryptedTmpFile.delete() + progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) + } + + closeQuietly(thumbnailReader) + } catch (e: IOException) { + throw FatalBackendException(e) + } + + return futureThumbnail + } + @Throws(BackendException::class) fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { val ciphertextFile = cryptoFile.cloudFile @@ -393,7 +459,7 @@ abstract class CryptoImplDecorator( val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) if (genThumbnail) { - startThumbnailGeneratorThread(diskCache, cacheKey, thumbnailReader) + startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader) } progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) @@ -444,7 +510,7 @@ abstract class CryptoImplDecorator( } } - private fun startThumbnailGeneratorThread(diskCache: DiskLruCache?, cacheKey: String, thumbnailReader: PipedInputStream): Future<*> { + private fun startThumbnailGeneratorThread(cryptoFile: CryptoFile, diskCache: DiskLruCache, cacheKey: String, thumbnailReader: PipedInputStream): Future<*> { return thumbnailExecutorService.submit { try { val options = BitmapFactory.Options() @@ -461,6 +527,8 @@ abstract class CryptoImplDecorator( } closeQuietly(thumbnailReader) + + cryptoFile.thumbnail = diskCache[cacheKey] } catch (e: Exception) { Timber.e("Bitmap generation crashed") } @@ -476,29 +544,49 @@ abstract class CryptoImplDecorator( } protected fun associateThumbnailIfInCache(list: List): List { + val completionService = ExecutorCompletionService(thumbnailExecutorService) if (isGenerateThumbnailsEnabled()) { val firstCryptoFile = list.find { it is CryptoFile } ?: return list val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list val diskCache = getLruCacheFor(cloudType) ?: return list + val l = mutableListOf>() + + var len = 0 list.forEach { cryptoNode -> - if (cryptoNode is CryptoFile && isImageMediaType(cryptoNode.name)) { - val cacheKey = generateCacheKey(cryptoNode.cloudFile) - val cacheFile = diskCache[cacheKey] - if (cacheFile != null) { - cryptoNode.thumbnail = cacheFile - } else { - // TODO - // force thumbnail generation (~PER FOLDER) - val trash = File.createTempFile(cryptoNode.name, ".temp", internalCache) - read(cryptoNode, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) - trash.delete() - } + if (cryptoNode is CryptoFile && cryptoNode.thumbnail == null && isImageMediaType(cryptoNode.name)) { + Timber.tag("THUMBNAIL").i("Add Thumbnail Generation Service Request") + len++ + completionService.submit { cacheOrGenerate(cryptoNode, diskCache) } } } + + var received = 0 + while (received < len) { + completionService.take(); // blocks if none available + received++ + } + Timber.tag("THUMBNAIL").i("WAITED ALL") } return list } + private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { + val cacheKey = generateCacheKey(cryptoFile) + val cacheFile = diskCache[cacheKey] + if (cacheFile != null) { + // Timber.tag("THUMBNAIL").i("THREAD - Associo") + cryptoFile.thumbnail = cacheFile + } else { + // TODO + // force thumbnail generation (~PER FOLDER) + // better usage of the file... + val trash = File.createTempFile(cryptoFile.name, ".temp", internalCache) + // Timber.tag("THUMBNAIL").i("THREAD - Scarico") + readGenerateThumbnail(cryptoFile, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD).get() + trash.delete() + } + } + private fun isGenerateThumbnailsEnabled(): Boolean { return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER } From 82525c0d3d3461475aec949a25efdd57fbeb7ab3 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 6 Oct 2024 12:41:53 +0200 Subject: [PATCH 24/29] Add automatic download and generation of thumbnails after scroll and fastScroll --- .../data/cloud/crypto/CryptoImplDecorator.kt | 11 ++-- .../presenter/BrowseFilesPresenter.kt | 54 +++++++++++++++++++ .../ui/fragment/BrowseFilesFragment.kt | 36 +++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index dc7864d08..91c47b639 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -390,7 +390,11 @@ abstract class CryptoImplDecorator( val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) - val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) + var genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) + diskCache?.let { disk -> + if (disk[cacheKey] != null) + genThumbnail = false + } val thumbnailWriter = PipedOutputStream() val thumbnailReader = PipedInputStream(thumbnailWriter) @@ -433,6 +437,7 @@ abstract class CryptoImplDecorator( } } finally { encryptedTmpFile.delete() + futureThumbnail.get() progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } @@ -528,6 +533,7 @@ abstract class CryptoImplDecorator( closeQuietly(thumbnailReader) + Timber.tag("THUMBNAIL").i("[FutureThumb] associate") cryptoFile.thumbnail = diskCache[cacheKey] } catch (e: Exception) { Timber.e("Bitmap generation crashed") @@ -571,10 +577,9 @@ abstract class CryptoImplDecorator( } private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { - val cacheKey = generateCacheKey(cryptoFile) + val cacheKey = generateCacheKey(cryptoFile.cloudFile) val cacheFile = diskCache[cacheKey] if (cacheFile != null) { - // Timber.tag("THUMBNAIL").i("THREAD - Associo") cryptoFile.thumbnail = cacheFile } else { // TODO diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index fa9e235f6..ef9463d67 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -80,6 +80,7 @@ import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.Workflow import org.cryptomator.util.ExceptionUtil import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.ThumbnailsOption import org.cryptomator.util.file.FileCacheUtils import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeTypes @@ -157,6 +158,59 @@ class BrowseFilesPresenter @Inject constructor( // @JvmField var openWritableFileNotification: OpenWritableFileNotification? = null + fun thumbnailsForVisibleNodes(visibleCloudNodes: List>) { + if (!sharedPreferencesHandler.useLruCache() || (sharedPreferencesHandler.generateThumbnails() == ThumbnailsOption.NEVER)) { + return + } + val toDownload = ArrayList() + visibleCloudNodes.forEach { node -> + if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail == null) { + toDownload.add(node) + } + } + if (toDownload.isEmpty()) { + return + } + downloadAndGenerateThumbnails(toDownload) + } + + private fun downloadAndGenerateThumbnails(visibleCloudFiles: List) { + view?.showProgress( + visibleCloudFiles, // + ProgressModel( + progressStateModelMapper.toModel( // + DownloadState.download(visibleCloudFiles[0].toCloudNode()) + ), 0 + ) + ) + downloadFilesUseCase // + .withDownloadFiles(downloadFileUtil.createDownloadFilesFor(this, visibleCloudFiles)) // + .run(object : DefaultProgressAwareResultHandler, DownloadState>() { + override fun onFinished() { + view?.hideProgress(visibleCloudFiles) + } + + override fun onProgress(progress: Progress) { + if (!progress.isOverallComplete) { + view?.showProgress( + cloudFileModelMapper.toModel(progress.state().file()), // + progressModelMapper.toModel(progress) + ) + } + if (progress.isCompleteAndHasState) { + val cloudFile = progress.state().file() + val cloudFileModel = cloudFileModelMapper.toModel(cloudFile) + view?.addOrUpdateCloudNode(cloudFileModel) + } + } + + override fun onError(e: Throwable) { + view?.hideProgress(visibleCloudFiles) + super.onError(e) + } + }) + } + override fun workflows(): Iterable> { return listOf(addExistingVaultWorkflow, createNewVaultWorkflow) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt index decd01a0e..870fda768 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt @@ -6,10 +6,13 @@ import android.util.TypedValue import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.simplecityapps.recyclerview_fastscroll.interfaces.OnFastScrollStateChangeListener import org.cryptomator.domain.CloudNode import org.cryptomator.generator.Fragment import org.cryptomator.presentation.R @@ -82,6 +85,27 @@ class BrowseFilesFragment : BaseFragment(FragmentBro } } + private val onFastScrollStateChangeListener = object : OnFastScrollStateChangeListener { + @Override + override fun onFastScrollStop() { + thumbnailsForVisibleNodes() + } + + @Override + override fun onFastScrollStart() { + } + } + + private val onScrollListener = object : RecyclerView.OnScrollListener() { + @Override + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == SCROLL_STATE_IDLE) { + thumbnailsForVisibleNodes() + } + } + } + val selectedCloudNodes: List> get() = cloudNodesAdapter.selectedCloudNodes() @@ -103,6 +127,8 @@ class BrowseFilesFragment : BaseFragment(FragmentBro binding.recyclerViewLayout.recyclerView.setHasFixedSize(true) binding.recyclerViewLayout.recyclerView.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 88f, resources.displayMetrics).toInt()) binding.recyclerViewLayout.recyclerView.clipToPadding = false + binding.recyclerViewLayout.recyclerView.setOnFastScrollStateChangeListener(onFastScrollStateChangeListener) + binding.recyclerViewLayout.recyclerView.addOnScrollListener(onScrollListener) browseFilesPresenter.onFolderRedisplayed(folder) @@ -114,6 +140,16 @@ class BrowseFilesFragment : BaseFragment(FragmentBro } } + private fun thumbnailsForVisibleNodes() { + val layoutManager = binding.recyclerViewLayout.recyclerView.layoutManager as LinearLayoutManager + val first = layoutManager.findFirstVisibleItemPosition() + val last = layoutManager.findLastVisibleItemPosition() + val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1) + if (!binding.swipeRefreshLayout.isRefreshing) { + browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes) + } + } + private fun isNavigationMode(navigationMode: ChooseCloudNodeSettings.NavigationMode): Boolean = this.navigationMode == navigationMode private fun setupNavigationMode() { From 034a29bbfcdd2f440c2e01350cec6a6a45286d16 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 6 Oct 2024 12:53:51 +0200 Subject: [PATCH 25/29] Add AssociateThumbanilUseCase to fetch thumbnails from cache not in Main Thread Created a new usecase to fetch thumbnails trat updates the UI with a progressAware --- .../crypto/CryptoCloudContentRepository.kt | 8 +- .../data/cloud/crypto/CryptoImplDecorator.kt | 115 ++++++++++++++---- .../cloud/crypto/CryptoImplVaultFormat7.kt | 2 - .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 2 - .../DispatchingCloudContentRepository.kt | 13 ++ .../repository/CloudContentRepository.kt | 7 ++ .../usecases/cloud/AssociateThumbnails.java | 27 ++++ .../presenter/BrowseFilesPresenter.kt | 37 +++++- 8 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt index 76da49932..a49c80a41 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt @@ -13,6 +13,7 @@ import org.cryptomator.domain.repository.CloudContentRepository import org.cryptomator.domain.usecases.ProgressAware import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.DownloadState +import org.cryptomator.domain.usecases.cloud.FileTransferState import org.cryptomator.domain.usecases.cloud.UploadState import java.io.File import java.io.OutputStream @@ -92,7 +93,12 @@ internal class CryptoCloudContentRepository(context: Context, cloudContentReposi @Throws(BackendException::class) override fun read(file: CryptoFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { - cryptoImpl.read(file, data, progressAware) + cryptoImpl.readGenerateThumbnail(file, data, progressAware) + } + + @Throws(BackendException::class) + override fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + return cryptoImpl.associateThumbnails(list, progressAware) } @Throws(BackendException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 91c47b639..10ad0ca6d 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -28,6 +28,7 @@ import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.DownloadState import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from +import org.cryptomator.domain.usecases.cloud.FileTransferState import org.cryptomator.domain.usecases.cloud.Progress import org.cryptomator.domain.usecases.cloud.UploadState import org.cryptomator.util.SharedPreferencesHandler @@ -50,13 +51,12 @@ import java.nio.channels.Channels import java.util.LinkedList import java.util.Queue import java.util.UUID -import java.util.concurrent.Callable import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorCompletionService import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.function.Supplier +import kotlin.system.measureTimeMillis import timber.log.Timber @@ -549,31 +549,98 @@ abstract class CryptoImplDecorator( return isGenerateThumbnailsEnabled() && cache != null && isImageMediaType(fileName) } - protected fun associateThumbnailIfInCache(list: List): List { - val completionService = ExecutorCompletionService(thumbnailExecutorService) - if (isGenerateThumbnailsEnabled()) { - val firstCryptoFile = list.find { it is CryptoFile } ?: return list - val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list - val diskCache = getLruCacheFor(cloudType) ?: return list - val l = mutableListOf>() - - var len = 0 - list.forEach { cryptoNode -> - if (cryptoNode is CryptoFile && cryptoNode.thumbnail == null && isImageMediaType(cryptoNode.name)) { - Timber.tag("THUMBNAIL").i("Add Thumbnail Generation Service Request") - len++ - completionService.submit { cacheOrGenerate(cryptoNode, diskCache) } - } - } + // TODO: remove me +// protected fun associateThumbnailIfInCache(list: List): List { +//// val completionService = ExecutorCompletionService(thumbnailExecutorService) +// if (!isGenerateThumbnailsEnabled()) { +// return list +// } +// val firstCryptoFile = list.find { it is CryptoFile } ?: return list +// val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list +// val diskCache = getLruCacheFor(cloudType) ?: return list +// +// val toProcess = list.filterIsInstance().filter { cryptoFile -> +// (isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null) +// } +// +// Timber.tag("THUMBNAIL").i("[Associate] origList.len:${list.size}, toProcessList.len:${toProcess.size}") +// var associated = 0 +// val elapsed = measureTimeMillis { +// toProcess.forEach { cryptoFile -> +// val cacheKey = generateCacheKey(cryptoFile.cloudFile) +// val cacheFile = diskCache[cacheKey] +// if (cacheFile != null) { +// cryptoFile.thumbnail = cacheFile +// associated++ +// } +// } +// } +// Timber.tag("THUMBNAIL").i("[Associate] associated:${associated} files, elapsed:${elapsed}ms") +// +// val countThumbnails = list.filterIsInstance().filter { cryptoFile -> cryptoFile.thumbnail != null }.count() +// Timber.tag("THUMBNAIL").i("[Associate] Num. file with thumbnail associated: $countThumbnails") +//// val l = mutableListOf>() +//// +//// var len = 0 +//// list.forEach { cryptoNode -> +//// if (cryptoNode is CryptoFile && cryptoNode.thumbnail == null && isImageMediaType(cryptoNode.name)) { +//// Timber.tag("THUMBNAIL").i("Add Thumbnail Generation Service Request") +//// len++ +//// completionService.submit { cacheOrGenerate(cryptoNode, diskCache) } +//// } +//// +//// var received = 0 +//// while (received < len) { +//// completionService.take(); // blocks if none available +//// received++ +//// } +//// Timber.tag("THUMBNAIL").i("WAITED ALL") +//// } +// return list +// } + + fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + if (!isGenerateThumbnailsEnabled()) { + return -1 + } + val cryptoFileList = list.filterIsInstance() + if (cryptoFileList.isEmpty()) { + return -1 + } + + val firstCryptoFile = cryptoFileList[0] + val cloudType = (firstCryptoFile).cloudFile.cloud?.type() ?: return -1 - var received = 0 - while (received < len) { - completionService.take(); // blocks if none available - received++ + val diskCache = getLruCacheFor(cloudType) ?: return -1 + val toProcess = cryptoFileList.filter { cryptoFile -> + (isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null) + } + +// Timber.tag("THUMBNAIL").i("[AssociateThumbnails] origList.len:${list.size}, toProcessList.len:${toProcess.size}") + var associated = 0 + val elapsed = measureTimeMillis { + toProcess.forEach { cryptoFile -> + val cacheKey = generateCacheKey(cryptoFile.cloudFile) + val cacheFile = diskCache.get(cacheKey) + if (cacheFile != null && cryptoFile.thumbnail == null) { + cryptoFile.thumbnail = cacheFile + associated++ + val state = FileTransferState { cryptoFile } + val progress = Progress.progress(state) // + .between(0) // + .and(toProcess.size.toLong()) // + .withValue(associated.toLong()) + + progressAware.onProgress(progress) + } } - Timber.tag("THUMBNAIL").i("WAITED ALL") } - return list + Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms") + + // val countThumbnails = cryptoFileList.count { cryptoFile -> cryptoFile.thumbnail != null } + // Timber.tag("THUMBNAIL").i("[AssociateThumbnails] Num. file with thumbnail associated: $countThumbnails") + + return associated } private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index c6fa3bdab..94fc5f815 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -165,8 +165,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { } }.map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.also { - associateThumbnailIfInCache(it) }.toList().filterNotNull() } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index a4b7328c8..0cc85a215 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -128,8 +128,6 @@ internal class CryptoImplVaultFormatPre7( .filterIsInstance() .map { node -> ciphertextToCleartextNode(cryptoFolder, dirId, node) - }.also { - associateThumbnailIfInCache(it) }.toList().filterNotNull() } diff --git a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt index 05b61418c..0085d5f19 100644 --- a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt @@ -14,6 +14,7 @@ import org.cryptomator.domain.repository.CloudContentRepository import org.cryptomator.domain.usecases.ProgressAware import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.DownloadState +import org.cryptomator.domain.usecases.cloud.FileTransferState import org.cryptomator.domain.usecases.cloud.UploadState import java.io.File import java.io.OutputStream @@ -164,6 +165,18 @@ class DispatchingCloudContentRepository @Inject constructor( } } + @Throws(BackendException::class) + override fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + return try { + // TODO: check if list empty + list[0].cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") + delegateFor(list[0]).associateThumbnails(list, progressAware) + } catch (e: AuthenticationException) { + delegates.remove(list[0].cloud) + throw e + } + } + @Throws(BackendException::class) override fun delete(node: CloudNode) { try { diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt index b7ecb0420..c697e497a 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt @@ -8,6 +8,7 @@ import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.usecases.ProgressAware import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.DownloadState +import org.cryptomator.domain.usecases.cloud.FileTransferState import org.cryptomator.domain.usecases.cloud.UploadState import java.io.File import java.io.OutputStream @@ -94,6 +95,12 @@ interface CloudContentRepository) + @Throws(BackendException::class) + fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + // default implementation + return -1 + } + @Throws(BackendException::class) fun delete(node: NodeType) diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java new file mode 100644 index 000000000..7f8ed9413 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java @@ -0,0 +1,27 @@ +package org.cryptomator.domain.usecases.cloud; + +import org.cryptomator.domain.CloudNode; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.ProgressAware; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; + +import java.util.List; + +@UseCase +public class AssociateThumbnails { + + private final CloudContentRepository cloudContentRepository; + private final List list; + + public AssociateThumbnails(CloudContentRepository cloudContentRepository, // + @Parameter List list) { + this.cloudContentRepository = cloudContentRepository; + this.list = list; + } + + public Integer execute(ProgressAware progressAware) throws BackendException { + return cloudContentRepository.associateThumbnails(list, progressAware); + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index ef9463d67..06ba96b09 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -25,10 +25,12 @@ import org.cryptomator.domain.usecases.CopyDataUseCase import org.cryptomator.domain.usecases.DownloadFile import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase import org.cryptomator.domain.usecases.ResultRenamed +import org.cryptomator.domain.usecases.cloud.AssociateThumbnailsUseCase import org.cryptomator.domain.usecases.cloud.CreateFolderUseCase import org.cryptomator.domain.usecases.cloud.DeleteNodesUseCase import org.cryptomator.domain.usecases.cloud.DownloadFilesUseCase import org.cryptomator.domain.usecases.cloud.DownloadState +import org.cryptomator.domain.usecases.cloud.FileTransferState import org.cryptomator.domain.usecases.cloud.GetCloudListRecursiveUseCase import org.cryptomator.domain.usecases.cloud.GetCloudListUseCase import org.cryptomator.domain.usecases.cloud.MoveFilesUseCase @@ -91,12 +93,14 @@ import java.security.DigestInputStream import java.security.MessageDigest import java.util.function.Supplier import javax.inject.Inject +import kotlin.math.min import kotlin.reflect.KClass import timber.log.Timber @PerView class BrowseFilesPresenter @Inject constructor( // private val getCloudListUseCase: GetCloudListUseCase, // + private val associateThumbnailsUseCase: AssociateThumbnailsUseCase, // private val createFolderUseCase: CreateFolderUseCase, // private val downloadFilesUseCase: DownloadFilesUseCase, // private val deleteNodesUseCase: DeleteNodesUseCase, // @@ -255,6 +259,7 @@ class BrowseFilesPresenter @Inject constructor( // clearCloudList() } else { showCloudNodesCollectionInView(cloudNodes) + associateThumbnails(cloudNodes) } view?.showLoading(false) } @@ -283,6 +288,35 @@ class BrowseFilesPresenter @Inject constructor( // }) } + private fun associateThumbnails(cloudNodes: List) { + associateThumbnailsUseCase.withList(cloudNodes) + .run(object : DefaultProgressAwareResultHandler() { + override fun onProgress(progress: Progress) { + Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onProgress") + val state = progress.state() + state?.let { state -> + view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(state.file())) + } + } + + override fun onSuccess(result: Int) { + Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onSuccess") + // no thumbnails were associated, start the generation of the first few + if (result == 0) { + val images = view?.renderedCloudNodes()?.filterIsInstance()?.filter { file -> isImageMediaType(file.name) } + images?.let { images -> + thumbnailsForVisibleNodes(images.subList(0, min(10, images.count()))) + } + } + } + + override fun onError(e: Throwable) { + Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onError") + super.onError(e) + } + }) + } + @Callback(dispatchResultOkOnly = false) fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) { if (result.isResultOk) { @@ -1376,7 +1410,8 @@ class BrowseFilesPresenter @Inject constructor( // copyDataUseCase, // moveFilesUseCase, // moveFoldersUseCase, // - getDecryptedCloudForVaultUseCase + getDecryptedCloudForVaultUseCase, // + associateThumbnailsUseCase ) this.authenticationExceptionHandler = authenticationExceptionHandler } From 80c013eb8d209147127e947ba24676173dac593c Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 6 Oct 2024 15:26:47 +0200 Subject: [PATCH 26/29] Unify the readGenerateThumbnail() with the original read() and minor fixes --- .../crypto/CryptoCloudContentRepository.kt | 2 +- .../data/cloud/crypto/CryptoImplDecorator.kt | 119 ++++-------------- .../presenter/BrowseFilesPresenter.kt | 7 -- 3 files changed, 22 insertions(+), 106 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt index a49c80a41..98285bc10 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt @@ -93,7 +93,7 @@ internal class CryptoCloudContentRepository(context: Context, cloudContentReposi @Throws(BackendException::class) override fun read(file: CryptoFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) { - cryptoImpl.readGenerateThumbnail(file, data, progressAware) + cryptoImpl.read(file, data, progressAware) } @Throws(BackendException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 10ad0ca6d..a1a34e70c 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -381,74 +381,6 @@ abstract class CryptoImplDecorator( } else name.substring(lastDot + 1) } - @Throws(BackendException::class) - fun readGenerateThumbnail(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware): Future<*> { - // TODO refactor this method with the real read - - val ciphertextFile = cryptoFile.cloudFile - var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null) - - val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } - val cacheKey = generateCacheKey(ciphertextFile) - var genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) - diskCache?.let { disk -> - if (disk[cacheKey] != null) - genThumbnail = false - } - - val thumbnailWriter = PipedOutputStream() - val thumbnailReader = PipedInputStream(thumbnailWriter) - - try { - val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) - - if (genThumbnail) { - futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader) - } - - progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) - try { - Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel -> - DecryptingReadableByteChannel(readableByteChannel, cryptor(), true).use { decryptingReadableByteChannel -> - val buff = ByteBuffer.allocate(cryptor().fileContentCryptor().ciphertextChunkSize()) - val cleartextSize = cryptoFile.size ?: Long.MAX_VALUE - var decrypted: Long = 0 - var read: Int - while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) { - buff.flip() - data.write(buff.array(), 0, buff.remaining()) - if (genThumbnail) { - thumbnailWriter.write(buff.array(), 0, buff.remaining()) - } - - decrypted += read.toLong() - - progressAware - .onProgress( - Progress.progress(DownloadState.decryption(cryptoFile)) // - .between(0) // - .and(cleartextSize) // - .withValue(decrypted) - ) - } - } - thumbnailWriter.flush() - closeQuietly(thumbnailWriter) - } - } finally { - encryptedTmpFile.delete() - futureThumbnail.get() - progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) - } - - closeQuietly(thumbnailReader) - } catch (e: IOException) { - throw FatalBackendException(e) - } - - return futureThumbnail - } - @Throws(BackendException::class) fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware) { val ciphertextFile = cryptoFile.cloudFile @@ -456,6 +388,7 @@ abstract class CryptoImplDecorator( val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } val cacheKey = generateCacheKey(ciphertextFile) val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) + var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null) val thumbnailWriter = PipedOutputStream() val thumbnailReader = PipedInputStream(thumbnailWriter) @@ -464,7 +397,7 @@ abstract class CryptoImplDecorator( val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware) if (genThumbnail) { - startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader) + futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader) } progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile))) @@ -498,6 +431,9 @@ abstract class CryptoImplDecorator( } } finally { encryptedTmpFile.delete() + if (genThumbnail) { + futureThumbnail.get() + } progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile))) } @@ -521,19 +457,15 @@ abstract class CryptoImplDecorator( val options = BitmapFactory.Options() val thumbnailBitmap: Bitmap? options.inSampleSize = 4 // pixel number reduced by a factor of 1/16 - val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options) val thumbnailWidth = 100 val thumbnailHeight = 100 thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight) - if (thumbnailBitmap != null) { storeThumbnail(diskCache, cacheKey, thumbnailBitmap) } - closeQuietly(thumbnailReader) - Timber.tag("THUMBNAIL").i("[FutureThumb] associate") cryptoFile.thumbnail = diskCache[cacheKey] } catch (e: Exception) { Timber.e("Bitmap generation crashed") @@ -616,48 +548,39 @@ abstract class CryptoImplDecorator( (isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null) } -// Timber.tag("THUMBNAIL").i("[AssociateThumbnails] origList.len:${list.size}, toProcessList.len:${toProcess.size}") var associated = 0 val elapsed = measureTimeMillis { toProcess.forEach { cryptoFile -> val cacheKey = generateCacheKey(cryptoFile.cloudFile) - val cacheFile = diskCache.get(cacheKey) + val cacheFile = diskCache[cacheKey] if (cacheFile != null && cryptoFile.thumbnail == null) { cryptoFile.thumbnail = cacheFile associated++ val state = FileTransferState { cryptoFile } - val progress = Progress.progress(state) // - .between(0) // - .and(toProcess.size.toLong()) // - .withValue(associated.toLong()) - + val progress = Progress.progress(state).thatIsCompleted() progressAware.onProgress(progress) } } } Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms") - // val countThumbnails = cryptoFileList.count { cryptoFile -> cryptoFile.thumbnail != null } - // Timber.tag("THUMBNAIL").i("[AssociateThumbnails] Num. file with thumbnail associated: $countThumbnails") - return associated } - private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { - val cacheKey = generateCacheKey(cryptoFile.cloudFile) - val cacheFile = diskCache[cacheKey] - if (cacheFile != null) { - cryptoFile.thumbnail = cacheFile - } else { - // TODO - // force thumbnail generation (~PER FOLDER) - // better usage of the file... - val trash = File.createTempFile(cryptoFile.name, ".temp", internalCache) - // Timber.tag("THUMBNAIL").i("THREAD - Scarico") - readGenerateThumbnail(cryptoFile, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD).get() - trash.delete() - } - } +// private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { +// val cacheKey = generateCacheKey(cryptoFile.cloudFile) +// val cacheFile = diskCache[cacheKey] +// if (cacheFile != null) { +// cryptoFile.thumbnail = cacheFile +// } else { +// // force thumbnail generation (~PER FOLDER) +// // better usage of the file... +// val trash = File.createTempFile(cryptoFile.name, ".temp", internalCache) +// // Timber.tag("THUMBNAIL").i("THREAD - Scarico") +// readGenerateThumbnail(cryptoFile, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD).get() +// trash.delete() +// } +// } private fun isGenerateThumbnailsEnabled(): Boolean { return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index 06ba96b09..b1e7f81f2 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -292,7 +292,6 @@ class BrowseFilesPresenter @Inject constructor( // associateThumbnailsUseCase.withList(cloudNodes) .run(object : DefaultProgressAwareResultHandler() { override fun onProgress(progress: Progress) { - Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onProgress") val state = progress.state() state?.let { state -> view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(state.file())) @@ -300,7 +299,6 @@ class BrowseFilesPresenter @Inject constructor( // } override fun onSuccess(result: Int) { - Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onSuccess") // no thumbnails were associated, start the generation of the first few if (result == 0) { val images = view?.renderedCloudNodes()?.filterIsInstance()?.filter { file -> isImageMediaType(file.name) } @@ -309,11 +307,6 @@ class BrowseFilesPresenter @Inject constructor( // } } } - - override fun onError(e: Throwable) { - Timber.tag("THUMBNAIL").i("[AssociateThumbnails] onError") - super.onError(e) - } }) } From 932d7d256a36e6ffaf8923c9658f2481fa293640 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Tue, 8 Oct 2024 18:50:08 +0200 Subject: [PATCH 27/29] Change thumbnail cachekey, add ThumbnailOption.READONLY and automatically generate thumbnails for latest photos --- .../crypto/CryptoCloudContentRepository.kt | 4 +-- .../data/cloud/crypto/CryptoImplDecorator.kt | 29 ++++++++----------- .../cloud/crypto/CryptoImplVaultFormat7.kt | 2 +- .../cloud/crypto/CryptoImplVaultFormatPre7.kt | 2 +- .../DispatchingCloudContentRepository.kt | 8 +++-- .../repository/CloudContentRepository.kt | 3 +- .../usecases/cloud/AssociateThumbnails.java | 4 +-- .../presenter/BrowseFilesPresenter.kt | 23 ++++++++------- presentation/src/main/res/values/arrays.xml | 2 ++ presentation/src/main/res/values/strings.xml | 6 ++-- presentation/src/main/res/xml/preferences.xml | 21 ++++++-------- .../util/SharedPreferencesHandler.kt | 3 +- .../org/cryptomator/util/ThumbnailsOption.kt | 1 + 13 files changed, 54 insertions(+), 54 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt index 98285bc10..48b00bc1a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt @@ -97,8 +97,8 @@ internal class CryptoCloudContentRepository(context: Context, cloudContentReposi } @Throws(BackendException::class) - override fun associateThumbnails(list: List, progressAware: ProgressAware): Int { - return cryptoImpl.associateThumbnails(list, progressAware) + override fun associateThumbnails(list: List, progressAware: ProgressAware) { + cryptoImpl.associateThumbnails(list, progressAware) } @Throws(BackendException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index a1a34e70c..58c5b4ed1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -100,8 +100,8 @@ abstract class CryptoImplDecorator( } protected fun renameFileInCache(source: CryptoFile, target: CryptoFile) { - val oldCacheKey = generateCacheKey(source.cloudFile) - val newCacheKey = generateCacheKey(target.cloudFile) + val oldCacheKey = generateCacheKey(source) + val newCacheKey = generateCacheKey(target) source.cloudFile.cloud?.type()?.let { cloudType -> getLruCacheFor(cloudType)?.let { diskCache -> if (diskCache[oldCacheKey] != null) { @@ -386,7 +386,7 @@ abstract class CryptoImplDecorator( val ciphertextFile = cryptoFile.cloudFile val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) } - val cacheKey = generateCacheKey(ciphertextFile) + val cacheKey = generateCacheKey(cryptoFile) val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name) var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null) @@ -473,12 +473,12 @@ abstract class CryptoImplDecorator( } } - protected fun generateCacheKey(cloudFile: CloudFile): String { - return String.format("%s-%d", cloudFile.cloud?.id() ?: "common", cloudFile.path.hashCode()) + protected fun generateCacheKey(cryptoFile: CryptoFile): String { + return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode()) } private fun isThumbnailGenerationAvailable(cache: DiskLruCache?, fileName: String): Boolean { - return isGenerateThumbnailsEnabled() && cache != null && isImageMediaType(fileName) + return isGenerateThumbnailsEnabled() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.READONLY && cache != null && isImageMediaType(fileName) } // TODO: remove me @@ -531,27 +531,24 @@ abstract class CryptoImplDecorator( // return list // } - fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + fun associateThumbnails(list: List, progressAware: ProgressAware) { if (!isGenerateThumbnailsEnabled()) { - return -1 + return } val cryptoFileList = list.filterIsInstance() if (cryptoFileList.isEmpty()) { - return -1 + return } - val firstCryptoFile = cryptoFileList[0] - val cloudType = (firstCryptoFile).cloudFile.cloud?.type() ?: return -1 - - val diskCache = getLruCacheFor(cloudType) ?: return -1 + val cloudType = (firstCryptoFile).cloudFile.cloud?.type() ?: return + val diskCache = getLruCacheFor(cloudType) ?: return val toProcess = cryptoFileList.filter { cryptoFile -> (isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null) } - var associated = 0 val elapsed = measureTimeMillis { toProcess.forEach { cryptoFile -> - val cacheKey = generateCacheKey(cryptoFile.cloudFile) + val cacheKey = generateCacheKey(cryptoFile) val cacheFile = diskCache[cacheKey] if (cacheFile != null && cryptoFile.thumbnail == null) { cryptoFile.thumbnail = cacheFile @@ -563,8 +560,6 @@ abstract class CryptoImplDecorator( } } Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms") - - return associated } // private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt index 94fc5f815..e22932d1d 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt @@ -450,7 +450,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator { cloudContentRepository.delete(node.cloudFile) } - val cacheKey = generateCacheKey(node.cloudFile) + val cacheKey = generateCacheKey(node) node.cloudFile.cloud?.type()?.let { cloudType -> getLruCacheFor(cloudType)?.let { diskCache -> if (diskCache[cacheKey] != null) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt index 0cc85a215..cc5f22c7b 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt @@ -248,7 +248,7 @@ internal class CryptoImplVaultFormatPre7( } else if (node is CryptoFile) { cloudContentRepository.delete(node.cloudFile) - val cacheKey = generateCacheKey(node.cloudFile) + val cacheKey = generateCacheKey(node) node.cloudFile.cloud?.type()?.let { cloudType -> getLruCacheFor(cloudType)?.let { diskCache -> if (diskCache[cacheKey] != null) { diff --git a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt index 0085d5f19..db4c330a4 100644 --- a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt @@ -166,9 +166,11 @@ class DispatchingCloudContentRepository @Inject constructor( } @Throws(BackendException::class) - override fun associateThumbnails(list: List, progressAware: ProgressAware): Int { - return try { - // TODO: check if list empty + override fun associateThumbnails(list: List, progressAware: ProgressAware) { + if (list.isEmpty()) { + return + } + try { list[0].cloud?.let { networkConnectionCheck.assertConnectionIsPresent(it) } ?: throw IllegalStateException("Parent's cloud shouldn't be null") delegateFor(list[0]).associateThumbnails(list, progressAware) } catch (e: AuthenticationException) { diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt index c697e497a..31bafe066 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt @@ -96,9 +96,8 @@ interface CloudContentRepository) @Throws(BackendException::class) - fun associateThumbnails(list: List, progressAware: ProgressAware): Int { + fun associateThumbnails(list: List, progressAware: ProgressAware) { // default implementation - return -1 } @Throws(BackendException::class) diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java index 7f8ed9413..84eec1c8e 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java @@ -21,7 +21,7 @@ public AssociateThumbnails(CloudContentRepository cloudContentRepository, // this.list = list; } - public Integer execute(ProgressAware progressAware) throws BackendException { - return cloudContentRepository.associateThumbnails(list, progressAware); + public void execute(ProgressAware progressAware) throws BackendException { + cloudContentRepository.associateThumbnails(list, progressAware); } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index b1e7f81f2..1238a3520 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -93,7 +93,6 @@ import java.security.DigestInputStream import java.security.MessageDigest import java.util.function.Supplier import javax.inject.Inject -import kotlin.math.min import kotlin.reflect.KClass import timber.log.Timber @@ -163,7 +162,7 @@ class BrowseFilesPresenter @Inject constructor( // var openWritableFileNotification: OpenWritableFileNotification? = null fun thumbnailsForVisibleNodes(visibleCloudNodes: List>) { - if (!sharedPreferencesHandler.useLruCache() || (sharedPreferencesHandler.generateThumbnails() == ThumbnailsOption.NEVER)) { + if (!sharedPreferencesHandler.useLruCache() || (sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.PER_FOLDER)) { return } val toDownload = ArrayList() @@ -289,8 +288,12 @@ class BrowseFilesPresenter @Inject constructor( // } private fun associateThumbnails(cloudNodes: List) { + if (!sharedPreferencesHandler.useLruCache() || sharedPreferencesHandler.generateThumbnails() == ThumbnailsOption.NEVER) { + return + } associateThumbnailsUseCase.withList(cloudNodes) - .run(object : DefaultProgressAwareResultHandler() { + .run(object : DefaultProgressAwareResultHandler() { + @Override override fun onProgress(progress: Progress) { val state = progress.state() state?.let { state -> @@ -298,13 +301,13 @@ class BrowseFilesPresenter @Inject constructor( // } } - override fun onSuccess(result: Int) { - // no thumbnails were associated, start the generation of the first few - if (result == 0) { - val images = view?.renderedCloudNodes()?.filterIsInstance()?.filter { file -> isImageMediaType(file.name) } - images?.let { images -> - thumbnailsForVisibleNodes(images.subList(0, min(10, images.count()))) - } + @Override + override fun onFinished() { + val images = view?.renderedCloudNodes()?.filterIsInstance()?.filter { file -> isImageMediaType(file.name) } ?: return + val firstImages = images.subList(0, 10) + val noThumbnailImages = firstImages.filter { img -> img.thumbnail == null } + if (noThumbnailImages.isNotEmpty()) { + thumbnailsForVisibleNodes(noThumbnailImages) } } }) diff --git a/presentation/src/main/res/values/arrays.xml b/presentation/src/main/res/values/arrays.xml index e1fbbca51..1136b29cd 100644 --- a/presentation/src/main/res/values/arrays.xml +++ b/presentation/src/main/res/values/arrays.xml @@ -44,11 +44,13 @@ @string/thumbnail_generation_never + @string/thumbnail_generation_readonly @string/thumbnail_generation_file @string/thumbnail_generation_folder "NEVER" + "READONLY" "PER_FILE" "PER_FOLDER" diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 3712bc4f8..32fd301b6 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -633,8 +633,9 @@ 5 GB Never - Per File - Per Folder + Read Only + Generate Per File + Generate Per Folder Style @@ -645,7 +646,6 @@ Once a day @string/lock_timeout_never - Thumbnails Thumbnail generation diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 403b44895..5f6c933ee 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -120,18 +120,6 @@ - - - - - + + diff --git a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt index 8eb93f180..4a7c5d7d3 100644 --- a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt +++ b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt @@ -162,8 +162,9 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen } fun generateThumbnails(): ThumbnailsOption { - return when(defaultSharedPreferences.getValue(THUMBNAIL_GENERATION, "NEVER")){ + return when (defaultSharedPreferences.getValue(THUMBNAIL_GENERATION, "NEVER")) { "NEVER" -> ThumbnailsOption.NEVER + "READONLY" -> ThumbnailsOption.READONLY "PER_FILE" -> ThumbnailsOption.PER_FILE "PER_FOLDER" -> ThumbnailsOption.PER_FOLDER else -> ThumbnailsOption.NEVER diff --git a/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt b/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt index 91fdc623b..e82c2500f 100644 --- a/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt +++ b/util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt @@ -2,6 +2,7 @@ package org.cryptomator.util enum class ThumbnailsOption { NEVER, + READONLY, PER_FILE, PER_FOLDER } \ No newline at end of file From d85096b5393618db56e4e9d2a724a48613d5588b Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 12 Oct 2024 11:06:14 +0200 Subject: [PATCH 28/29] Fix thumbnail settings and clean up --- .../data/cloud/crypto/CryptoImplDecorator.kt | 67 +------------------ .../ui/fragment/SettingsFragment.kt | 24 +++---- presentation/src/main/res/values/arrays.xml | 8 +-- presentation/src/main/res/values/strings.xml | 1 + presentation/src/main/res/xml/preferences.xml | 2 +- 5 files changed, 19 insertions(+), 83 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 58c5b4ed1..3aa3b5165 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -481,56 +481,6 @@ abstract class CryptoImplDecorator( return isGenerateThumbnailsEnabled() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.READONLY && cache != null && isImageMediaType(fileName) } - // TODO: remove me -// protected fun associateThumbnailIfInCache(list: List): List { -//// val completionService = ExecutorCompletionService(thumbnailExecutorService) -// if (!isGenerateThumbnailsEnabled()) { -// return list -// } -// val firstCryptoFile = list.find { it is CryptoFile } ?: return list -// val cloudType = (firstCryptoFile as CryptoFile).cloudFile.cloud?.type() ?: return list -// val diskCache = getLruCacheFor(cloudType) ?: return list -// -// val toProcess = list.filterIsInstance().filter { cryptoFile -> -// (isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null) -// } -// -// Timber.tag("THUMBNAIL").i("[Associate] origList.len:${list.size}, toProcessList.len:${toProcess.size}") -// var associated = 0 -// val elapsed = measureTimeMillis { -// toProcess.forEach { cryptoFile -> -// val cacheKey = generateCacheKey(cryptoFile.cloudFile) -// val cacheFile = diskCache[cacheKey] -// if (cacheFile != null) { -// cryptoFile.thumbnail = cacheFile -// associated++ -// } -// } -// } -// Timber.tag("THUMBNAIL").i("[Associate] associated:${associated} files, elapsed:${elapsed}ms") -// -// val countThumbnails = list.filterIsInstance().filter { cryptoFile -> cryptoFile.thumbnail != null }.count() -// Timber.tag("THUMBNAIL").i("[Associate] Num. file with thumbnail associated: $countThumbnails") -//// val l = mutableListOf>() -//// -//// var len = 0 -//// list.forEach { cryptoNode -> -//// if (cryptoNode is CryptoFile && cryptoNode.thumbnail == null && isImageMediaType(cryptoNode.name)) { -//// Timber.tag("THUMBNAIL").i("Add Thumbnail Generation Service Request") -//// len++ -//// completionService.submit { cacheOrGenerate(cryptoNode, diskCache) } -//// } -//// -//// var received = 0 -//// while (received < len) { -//// completionService.take(); // blocks if none available -//// received++ -//// } -//// Timber.tag("THUMBNAIL").i("WAITED ALL") -//// } -// return list -// } - fun associateThumbnails(list: List, progressAware: ProgressAware) { if (!isGenerateThumbnailsEnabled()) { return @@ -561,22 +511,7 @@ abstract class CryptoImplDecorator( } Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms") } - -// private fun cacheOrGenerate(cryptoFile: CryptoFile, diskCache: DiskLruCache) { -// val cacheKey = generateCacheKey(cryptoFile.cloudFile) -// val cacheFile = diskCache[cacheKey] -// if (cacheFile != null) { -// cryptoFile.thumbnail = cacheFile -// } else { -// // force thumbnail generation (~PER FOLDER) -// // better usage of the file... -// val trash = File.createTempFile(cryptoFile.name, ".temp", internalCache) -// // Timber.tag("THUMBNAIL").i("THREAD - Scarico") -// readGenerateThumbnail(cryptoFile, trash.outputStream(), ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD).get() -// trash.delete() -// } -// } - + private fun isGenerateThumbnailsEnabled(): Boolean { return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index 355df440e..4fe3375fd 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -9,6 +9,7 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.biometric.BiometricManager import androidx.core.content.ContextCompat +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -47,7 +48,6 @@ class SettingsFragment : PreferenceFragmentCompat() { setupAppVersion() setupLruCacheSize() setupLicense() - setupThumbnailGeneration() setupCryptomatorVariants() } @@ -81,6 +81,17 @@ class SettingsFragment : PreferenceFragmentCompat() { if (FALSE == newValue) { LruFileCacheUtil(requireContext()).clear() setupLruCacheSize() + + findPreference(THUMBNAIL_GENERATION)?.let { preference -> + preference.isSelectable = false + } + Toast.makeText(context, context?.getString(R.string.thumbnail_generation__deactivation_toast), Toast.LENGTH_LONG).show() + } + + if (TRUE == newValue) { + findPreference(THUMBNAIL_GENERATION)?.let { preference -> + preference.isSelectable = true + } } Toast.makeText(context, context?.getString(R.string.screen_settings_lru_cache_changed__restart_toast), Toast.LENGTH_SHORT).show() @@ -110,11 +121,6 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - // TODO ... - true - } - private fun activity(): SettingsActivity = this.activity as SettingsActivity private fun isBiometricAuthenticationNotAvailableRemovePreference() { @@ -146,11 +152,6 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - private fun setupThumbnailGeneration() { - val preference = findPreference(THUMBNAIL_GENERATION) as Preference? - // TODO ... - } - private fun setupLruCacheSize() { val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) as Preference? val size = LruFileCacheUtil(requireContext()).totalSize() @@ -255,7 +256,6 @@ class SettingsFragment : PreferenceFragmentCompat() { } (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD_VAULT) as Preference?)?.intent = Intent(context, AutoUploadChooseVaultActivity::class.java) (findPreference(SharedPreferencesHandler.LICENSES_ACTIVITY) as Preference?)?.intent = Intent(context, LicensesActivity::class.java) - (findPreference(SharedPreferencesHandler.THUMBNAIL_GENERATION) as Preference?)?.onPreferenceChangeListener = thumbnailGenerationChangeListener } fun deactivateDebugMode() { diff --git a/presentation/src/main/res/values/arrays.xml b/presentation/src/main/res/values/arrays.xml index 1136b29cd..d93229e38 100644 --- a/presentation/src/main/res/values/arrays.xml +++ b/presentation/src/main/res/values/arrays.xml @@ -49,10 +49,10 @@ @string/thumbnail_generation_folder - "NEVER" - "READONLY" - "PER_FILE" - "PER_FOLDER" + NEVER + READONLY + PER_FILE + PER_FOLDER diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 32fd301b6..2a17f3392 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -636,6 +636,7 @@ Read Only Generate Per File Generate Per Folder + LRU cache disabled therefore also the thumbnails Style diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 5f6c933ee..5f4ba441c 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -142,7 +142,7 @@ android:title="@string/screen_settings_lru_cache_size" /> Date: Mon, 14 Oct 2024 17:41:24 +0200 Subject: [PATCH 29/29] Minor fixes :) --- .../data/cloud/crypto/CryptoImplDecorator.kt | 11 ++++++++--- .../presentation/presenter/BrowseFilesPresenter.kt | 10 ++++------ .../presentation/ui/fragment/BrowseFilesFragment.kt | 6 +++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt index 3aa3b5165..f77259b50 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt @@ -121,7 +121,7 @@ abstract class CryptoImplDecorator( CloudType.WEBDAV -> LruFileCacheUtil.Cache.WEBDAV CloudType.S3 -> LruFileCacheUtil.Cache.S3 CloudType.LOCAL -> LruFileCacheUtil.Cache.LOCAL - else -> throw IllegalStateException() + else -> throw IllegalStateException("Unexpected CloudType: $type") } } @@ -458,6 +458,11 @@ abstract class CryptoImplDecorator( val thumbnailBitmap: Bitmap? options.inSampleSize = 4 // pixel number reduced by a factor of 1/16 val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options) + if (bitmap == null) { + closeQuietly(thumbnailReader) + return@submit + } + val thumbnailWidth = 100 val thumbnailHeight = 100 thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight) @@ -468,7 +473,7 @@ abstract class CryptoImplDecorator( cryptoFile.thumbnail = diskCache[cacheKey] } catch (e: Exception) { - Timber.e("Bitmap generation crashed") + Timber.e(e, "Bitmap generation crashed") } } } @@ -511,7 +516,7 @@ abstract class CryptoImplDecorator( } Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms") } - + private fun isGenerateThumbnailsEnabled(): Boolean { return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt index 1238a3520..bd7673b04 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -293,7 +293,6 @@ class BrowseFilesPresenter @Inject constructor( // } associateThumbnailsUseCase.withList(cloudNodes) .run(object : DefaultProgressAwareResultHandler() { - @Override override fun onProgress(progress: Progress) { val state = progress.state() state?.let { state -> @@ -301,13 +300,12 @@ class BrowseFilesPresenter @Inject constructor( // } } - @Override override fun onFinished() { val images = view?.renderedCloudNodes()?.filterIsInstance()?.filter { file -> isImageMediaType(file.name) } ?: return - val firstImages = images.subList(0, 10) - val noThumbnailImages = firstImages.filter { img -> img.thumbnail == null } - if (noThumbnailImages.isNotEmpty()) { - thumbnailsForVisibleNodes(noThumbnailImages) + images.take(10).filter { img -> img.thumbnail == null }.let { firstImagesWithoutThumbnails -> + if (firstImagesWithoutThumbnails.isNotEmpty()) { + thumbnailsForVisibleNodes(firstImagesWithoutThumbnails) + } } } }) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt index 870fda768..45ae99674 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt @@ -6,11 +6,12 @@ import android.util.TypedValue import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.simplecityapps.recyclerview_fastscroll.interfaces.OnFastScrollStateChangeListener import org.cryptomator.domain.CloudNode @@ -144,6 +145,9 @@ class BrowseFilesFragment : BaseFragment(FragmentBro val layoutManager = binding.recyclerViewLayout.recyclerView.layoutManager as LinearLayoutManager val first = layoutManager.findFirstVisibleItemPosition() val last = layoutManager.findLastVisibleItemPosition() + if (first == NO_POSITION || last == NO_POSITION) { + return + } val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1) if (!binding.swipeRefreshLayout.isRefreshing) { browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes)