From 9ab203c67b0a1acae1201e3c970d4f5c0a9a2df2 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Thu, 11 Apr 2024 14:45:43 +0200 Subject: [PATCH 01/30] 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 | 55 +++++++++++++++- .../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, 202 insertions(+), 36 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 f32cd3e32..7700e2e34 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 29a8f2f31..e0e18680d 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 ec397f7fc..55731f1ad 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -116,42 +116,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 8e31b644a..f0a048b5c 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,6 +1,13 @@ 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.util.Size import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -27,6 +34,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 import kotlinx.android.synthetic.main.item_browse_files_node.view.cloudNodeImage import kotlinx.android.synthetic.main.item_browse_files_node.view.itemCheckBox @@ -40,13 +52,16 @@ import kotlinx.android.synthetic.main.view_cloud_file_progress.view.cloudFile import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderActionText import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderContent import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderText +import timber.log.Timber class BrowseFilesAdapter @Inject 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>(CloudNodeModelNameAZComparator()), FastScrollRecyclerView.SectionedAdapter { private var chooseCloudNodeSettings: ChooseCloudNodeSettings? = null @@ -124,13 +139,14 @@ constructor( } inner class VaultContentViewHolder internal constructor(itemView: View) : RecyclerViewBaseAdapter<*, *, *>.ItemViewHolder(itemView) { - private var uiState: UiStateTest? = null private var currentProgressIcon: Int = 0 private var bound: CloudNodeModel<*>? = null +// private var diskLruCache: DiskLruCache? = null + override fun bind(position: Int) { bound = getItem(position) bound?.let { internalBind(it) } @@ -143,8 +159,41 @@ 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<*>) { - itemView.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 6a98cd7ed..1dfa60247 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 @@ -26,6 +27,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 import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton 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 4964cd445..803ed7fe7 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 bae69bc31..217fb5536 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -120,6 +120,18 @@ + + + + + Unit) { From 9fa1f832868fdb6131add9f46c797ea3b9c740b7 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 13 Apr 2024 16:51:26 +0200 Subject: [PATCH 02/30] 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 | 33 +---- .../ui/bottomsheet/FileSettingsBottomSheet.kt | 9 +- .../ui/fragment/BrowseFilesFragment.kt | 2 +- presentation/src/main/res/values/strings.xml | 6 + 11 files changed, 160 insertions(+), 116 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 7700e2e34..17052d105 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 f50d78645..d7f23b60c 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() } @@ -492,7 +507,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 e0e18680d..173a700b2 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 55731f1ad..ec397f7fc 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -116,42 +116,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 f0a048b5c..21dc58b20 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 @@ -60,7 +60,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>(CloudNodeModelNameAZComparator()), FastScrollRecyclerView.SectionedAdapter { @@ -145,8 +144,6 @@ constructor( private var bound: CloudNodeModel<*>? = null -// private var diskLruCache: DiskLruCache? = null - override fun bind(position: Int) { bound = getItem(position) bound?.let { internalBind(it) } @@ -159,39 +156,15 @@ 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<*>) { -// 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 e03d706af..5b060731d 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 @@ -33,7 +34,13 @@ class FileSettingsBottomSheet : BaseBottomSheet1 GB 5 GB + Never + Per File + Per Folder + Style Automatic (follow system) @@ -632,5 +636,7 @@ Once a day @string/lock_timeout_never + Thumbnails + Thumbnail generation From f158359ea7ad762a23a43fe6263de4bad1cb34d6 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sat, 13 Apr 2024 19:42:00 +0200 Subject: [PATCH 03/30] 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 d7f23b60c..2d3260b1a 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 a5da6b16d6b894d77015157cb684184b561062c4 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sun, 14 Apr 2024 12:22:23 +0200 Subject: [PATCH 04/30] 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 17052d105..0146367d2 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 2d3260b1a..9d936ca41 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 4bbd29c369c434fec7bdf4a562a4e299e2a8ebde Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Fri, 19 Apr 2024 09:44:09 +0200 Subject: [PATCH 05/30] 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 9d936ca41..0668921d1 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 46fcb27f3a9f78ce9a8087e9d9e9d053eb8920ab Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Sun, 21 Apr 2024 22:18:16 +0200 Subject: [PATCH 06/30] 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 0146367d2..776cfcbd8 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 0668921d1..fae2d04ef 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 7c95cad34d41ff0a45aec0e8e0cc4b5ddeeddd10 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 21 Apr 2024 22:26:49 +0200 Subject: [PATCH 07/30] Removed unused imports + fix typo --- .../presentation/ui/adapter/BrowseFilesAdapter.kt | 11 +---------- .../org/cryptomator/util/file/LruFileCacheUtil.kt | 2 +- 2 files changed, 2 insertions(+), 11 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 21dc58b20..273dfc9e7 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,13 +1,7 @@ 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.util.Size import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -34,11 +28,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 import kotlinx.android.synthetic.main.item_browse_files_node.view.cloudNodeImage import kotlinx.android.synthetic.main.item_browse_files_node.view.itemCheckBox @@ -52,7 +43,6 @@ import kotlinx.android.synthetic.main.view_cloud_file_progress.view.cloudFile import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderActionText import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderContent import kotlinx.android.synthetic.main.view_cloud_folder_content.view.cloudFolderText -import timber.log.Timber class BrowseFilesAdapter @Inject constructor( @@ -138,6 +128,7 @@ constructor( } inner class VaultContentViewHolder internal constructor(itemView: View) : RecyclerViewBaseAdapter<*, *, *>.ItemViewHolder(itemView) { + private var uiState: UiStateTest? = null private var currentProgressIcon: Int = 0 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 6f5d2da30727791f05f8f0b3c4e2e08b64bed513 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 17:44:23 +0200 Subject: [PATCH 08/30] 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 776cfcbd8..b361ab887 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 91c153774921dc5be0e0a626918ee3b37a2156f4 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 18:36:10 +0200 Subject: [PATCH 09/30] 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 b361ab887..7b1d23f58 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 54499a0d3a8098d573006639a5689be49e58d1e7 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 28 Apr 2024 18:43:46 +0200 Subject: [PATCH 10/30] 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 fae2d04ef..d37e61ff1 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 bba7877334fe918585d56fe42d83761e5f4807cc Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Mon, 29 Apr 2024 18:19:14 +0200 Subject: [PATCH 11/30] 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 776cfcbd8..65f4b1a7c 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 @@ -372,7 +373,6 @@ abstract class CryptoImplDecorator( val genThumbnail = isGenerateThumbnailsEnabled(diskCache, cryptoFile.name) - // TODO: solo se e' un file immagine!!! val decryptedTempFile : File try { @@ -429,7 +429,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 0b14f9a60..df70b4849 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 f6ded14dec8596c53ca7f0a674d0230eb60ff3b1 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Mon, 29 Apr 2024 22:08:45 +0200 Subject: [PATCH 12/30] 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 2b3dbcee3..7251a511c 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 4fa867036fcc4935bed256223a039223f37076cc Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 8 May 2024 00:41:53 +0200 Subject: [PATCH 13/30] 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 7251a511c..0e18f2fc1 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 e67edf81296303aa73b30b4c462514126a0f39a1 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 8 May 2024 01:05:56 +0200 Subject: [PATCH 14/30] 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 0e18f2fc1..c7706f3c0 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 d37e61ff1..1e8336310 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 23f4f830bab54156decc532ae01aa16dd247c01a Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Mon, 29 Apr 2024 00:37:19 +0200 Subject: [PATCH 15/30] 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 c7706f3c0..33a2995b7 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 fcdc306e78ac8805212b74c52c030b938c189dab Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Wed, 1 May 2024 12:32:45 +0200 Subject: [PATCH 16/30] 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 33a2995b7..a0f0baa52 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 8a2b3d663a3571b03e1abba81e1401d6cc8a2c66 Mon Sep 17 00:00:00 2001 From: taglioIsCoding Date: Wed, 8 May 2024 11:50:24 +0200 Subject: [PATCH 17/30] 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 a0f0baa52..551e1d45e 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 1e8336310..206ac3f4b 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 c835dfb4e62868c1217a2b11904108fde981173d Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Fri, 3 May 2024 01:11:09 +0200 Subject: [PATCH 18/30] Add common interface for BrowseFilesFragment --- .../ui/fragment/BrowseFilesFragment.kt | 37 +++++++++---------- .../ui/fragment/FilesFragmentInterface.kt | 29 +++++++++++++++ 2 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/FilesFragmentInterface.kt 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 e227972ed..a5485772f 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 @@ -26,7 +26,6 @@ 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 import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton @@ -39,7 +38,7 @@ import kotlinx.android.synthetic.main.view_browses_files_extra_text_and_button.e import kotlinx.android.synthetic.main.view_empty_folder.emptyFolderHint @Fragment(R.layout.fragment_browse_files) -class BrowseFilesFragment : BaseFragment() { +open class BrowseFilesFragment : BaseFragment(), FilesFragmentInterface { @Inject lateinit var cloudNodesAdapter: BrowseFilesAdapter @@ -51,7 +50,7 @@ class BrowseFilesFragment : BaseFragment() { private var filterText: String = "" - var folder: CloudFolderModel + override var folder: CloudFolderModel get() = requireArguments().getSerializable(ARG_FOLDER) as CloudFolderModel set(updatedFolder) { arguments?.putSerializable(ARG_FOLDER, updatedFolder) @@ -90,7 +89,7 @@ class BrowseFilesFragment : BaseFragment() { } } - val selectedCloudNodes: List> + override val selectedCloudNodes: List> get() = cloudNodesAdapter.selectedCloudNodes() override fun setupView() { @@ -185,19 +184,19 @@ class BrowseFilesFragment : BaseFragment() { browseFilesPresenter.onFolderReloadContent(folder) } - fun show(nodes: List>?) { + override fun show(nodes: List>?) { cloudNodesAdapter.clear() cloudNodesAdapter.addAll(cloudNodesAdapter.filterNodes(nodes, filterText)) updateEmptyFolderHint() } - fun showProgress(nodes: List>?, progress: ProgressModel?) { + override fun showProgress(nodes: List>?, progress: ProgressModel?) { nodes?.forEach { node -> showProgress(node, progress) } } - fun showProgress(node: CloudNodeModel<*>?, progress: ProgressModel?) { + override fun showProgress(node: CloudNodeModel<*>?, progress: ProgressModel?) { val viewHolder = viewHolderFor(node) if (viewHolder.isPresent) { viewHolder.get().showProgress(progress) @@ -207,13 +206,13 @@ class BrowseFilesFragment : BaseFragment() { } } - fun hideProgress(nodes: List>?) { + override fun hideProgress(nodes: List>?) { nodes?.forEach { node -> hideProgress(node) } } - fun hideProgress(cloudNode: CloudNodeModel<*>?) { + override fun hideProgress(cloudNode: CloudNodeModel<*>?) { val viewHolder = viewHolderFor(cloudNode) if (viewHolder.isPresent) { viewHolder.get().hideProgress() @@ -223,7 +222,7 @@ class BrowseFilesFragment : BaseFragment() { } } - fun selectAllItems() { + override fun selectAllItems() { val hasUnSelectedNode = cloudNodesAdapter.hasUnSelectedNode() cloudNodesAdapter.renderedCloudNodes().forEach { node -> selectNode(node, hasUnSelectedNode) @@ -240,7 +239,7 @@ class BrowseFilesFragment : BaseFragment() { } } - fun remove(cloudNode: List>?) { + override fun remove(cloudNode: List>?) { cloudNodesAdapter.deleteItems(cloudNode) updateEmptyFolderHint() } @@ -250,15 +249,15 @@ class BrowseFilesFragment : BaseFragment() { return Optional.ofNullable(recyclerView.findViewHolderForAdapterPosition(positionOf) as? BrowseFilesAdapter.VaultContentViewHolder) } - fun replaceRenamedCloudFile(cloudFile: CloudNodeModel) { + override fun replaceRenamedCloudFile(cloudFile: CloudNodeModel) { cloudNodesAdapter.replaceRenamedCloudFile(cloudFile) } - fun showLoading(loading: Boolean?) { + override fun showLoading(loading: Boolean?) { loading?.let { swipeRefreshLayout.isRefreshing = it } } - fun addOrUpdate(cloudNode: CloudNodeModel<*>) { + override fun addOrUpdate(cloudNode: CloudNodeModel<*>) { cloudNodesAdapter.addOrReplaceCloudNode(cloudNode) updateEmptyFolderHint() } @@ -277,11 +276,11 @@ class BrowseFilesFragment : BaseFragment() { private fun isSelectionMode(selectionMode: ChooseCloudNodeSettings.SelectionMode): Boolean = chooseCloudNodeSettings?.selectionMode() == selectionMode - fun renderedCloudNodes(): List> = cloudNodesAdapter.renderedCloudNodes() + override fun renderedCloudNodes(): List> = cloudNodesAdapter.renderedCloudNodes() - fun rootView(): View = slidingCoordinatorLayout + override fun rootView(): View = slidingCoordinatorLayout - fun navigationModeChanged(navigationMode: ChooseCloudNodeSettings.NavigationMode) { + override fun navigationModeChanged(navigationMode: ChooseCloudNodeSettings.NavigationMode) { updateNavigationMode(navigationMode) if (navigationMode == SELECT_ITEMS) { @@ -296,11 +295,11 @@ class BrowseFilesFragment : BaseFragment() { cloudNodesAdapter.updateNavigationMode(navigationMode) } - fun setFilterText(query: String) { + override fun setFilterText(query: String) { filterText = query } - fun setSort(comparator: Comparator>) { + override fun setSort(comparator: Comparator>) { cloudNodesAdapter.setSort(comparator) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/FilesFragmentInterface.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/FilesFragmentInterface.kt new file mode 100644 index 000000000..d5cb811c9 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/FilesFragmentInterface.kt @@ -0,0 +1,29 @@ +package org.cryptomator.presentation.ui.fragment + +import org.cryptomator.domain.CloudNode +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings +import org.cryptomator.presentation.model.CloudNodeModel +import org.cryptomator.presentation.model.CloudFolderModel +import org.cryptomator.presentation.model.ProgressModel +import android.view.View +interface FilesFragmentInterface { + + abstract val selectedCloudNodes: List> + abstract var folder: CloudFolderModel + + abstract fun rootView(): View + fun selectAllItems() + abstract fun setSort(comparator: Comparator>) + abstract fun renderedCloudNodes(): List> + abstract fun navigationModeChanged(navigationMode: ChooseCloudNodeSettings.NavigationMode) + abstract fun show(nodes: List>?) + abstract fun addOrUpdate(cloudNode: CloudNodeModel<*>) + abstract fun remove(cloudNode: List>?) + abstract fun replaceRenamedCloudFile(cloudFile: CloudNodeModel) + abstract fun showProgress(node: CloudNodeModel<*>?, progress: ProgressModel?) + abstract fun showProgress(nodes: List>?, progress: ProgressModel?) + abstract fun hideProgress(cloudNode : CloudNodeModel<*>?) + abstract fun hideProgress(nodes : List>?) + abstract fun showLoading(loading: Boolean?) + abstract fun setFilterText(query: String) +} \ No newline at end of file From f394167778ce24dcbc0578233361a130393dcb9b Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Fri, 3 May 2024 01:13:01 +0200 Subject: [PATCH 19/30] Add GalleryFragment (same interface as BrowseFileFragment) and his Adapter --- .../di/component/ActivityComponent.java | 3 + .../ui/adapter/GalleryFilesAdapter.kt | 478 ++++++++++++++++++ .../ui/fragment/GalleryFragment.kt | 336 ++++++++++++ .../main/res/layout/fragment_gallery_view.xml | 51 ++ .../res/layout/item_gallery_files_node.xml | 50 ++ 5 files changed, 918 insertions(+) create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt create mode 100644 presentation/src/main/res/layout/fragment_gallery_view.xml create mode 100644 presentation/src/main/res/layout/item_gallery_files_node.xml diff --git a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java index ed3206b03..3b55b9fda 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java +++ b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java @@ -32,6 +32,7 @@ import org.cryptomator.presentation.ui.fragment.ChooseCloudServiceFragment; import org.cryptomator.presentation.ui.fragment.CloudConnectionListFragment; import org.cryptomator.presentation.ui.fragment.CloudSettingsFragment; +import org.cryptomator.presentation.ui.fragment.GalleryFragment; import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment; import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment; import org.cryptomator.presentation.ui.fragment.SetPasswordFragment; @@ -75,6 +76,8 @@ public interface ActivityComponent { void inject(BrowseFilesFragment browseFilesFragment); + void inject(GalleryFragment galleryFragment); + void inject(ChooseCloudServiceFragment chooseCloudServiceFragment); void inject(SharedFilesActivity sharedFilesActivity); diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt new file mode 100644 index 000000000..b18fa9fd6 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt @@ -0,0 +1,478 @@ +package org.cryptomator.presentation.ui.adapter + +import android.graphics.BitmapFactory +import android.os.PatternMatcher +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView +import org.cryptomator.domain.CloudNode +import org.cryptomator.presentation.R +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.NavigationMode.BROWSE_FILES +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.NavigationMode.SELECT_ITEMS +import org.cryptomator.presentation.model.CloudFileModel +import org.cryptomator.presentation.model.CloudFolderModel +import org.cryptomator.presentation.model.CloudNodeModel +import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.model.ProgressStateModel.Companion.COMPLETED +import org.cryptomator.presentation.model.comparator.CloudNodeModelDateNewestFirstComparator +import org.cryptomator.presentation.model.comparator.CloudNodeModelDateOldestFirstComparator +import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeBiggestFirstComparator +import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeSmallestFirstComparator +import org.cryptomator.presentation.ui.adapter.GalleryFilesAdapter.GalleryContentViewHolder +import org.cryptomator.presentation.util.DateHelper +import org.cryptomator.presentation.util.FileIcon +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.MimeType +import org.cryptomator.util.file.MimeTypes +import javax.inject.Inject +import kotlinx.android.synthetic.main.item_gallery_files_node.view.cloudFileProgress +import kotlinx.android.synthetic.main.item_gallery_files_node.view.galleryCloudNodeImage +import kotlinx.android.synthetic.main.item_gallery_files_node.view.progressIcon +import kotlinx.android.synthetic.main.view_cloud_file_progress.view.cloudFile + +class GalleryFilesAdapter @Inject +constructor( + private val dateHelper: DateHelper, // + private val fileSizeHelper: FileSizeHelper, // + private val fileUtil: FileUtil, // + private val sharedPreferencesHandler: SharedPreferencesHandler, // + private val mimeTypes: MimeTypes // +) : RecyclerViewBaseAdapter, GalleryFilesAdapter.ItemClickListener, GalleryContentViewHolder>(CloudNodeModelDateNewestFirstComparator()), FastScrollRecyclerView.SectionedAdapter { + + private var chooseCloudNodeSettings: ChooseCloudNodeSettings? = null + private var navigationMode: ChooseCloudNodeSettings.NavigationMode? = null + + private val isInSelectionMode: Boolean + get() = chooseCloudNodeSettings != null + + override fun getItemLayout(viewType: Int): Int { + return R.layout.item_gallery_files_node + } + + override fun createViewHolder(view: View, viewType: Int): GalleryContentViewHolder { + return GalleryContentViewHolder(view) + } + + fun addOrReplaceCloudNode(cloudNodeModel: CloudNodeModel<*>) { + if (contains(cloudNodeModel)) { + replaceItem(cloudNodeModel) + } else { + addItem(cloudNodeModel) + } + } + + fun replaceRenamedCloudFile(cloudNode: CloudNodeModel) { + itemCollection.forEach { nodes -> + if (nodes.javaClass == cloudNode.javaClass && nodes.name == cloudNode.oldName) { + val position = positionOf(nodes) + replaceItem(position, cloudNode) + return + } + } + } + + override fun setCallback(callback: ItemClickListener) { + this.callback = callback + } + + fun setChooseCloudNodeSettings(chooseCloudNodeSettings: ChooseCloudNodeSettings?) { + this.chooseCloudNodeSettings = chooseCloudNodeSettings + } + + fun updateNavigationMode(navigationMode: ChooseCloudNodeSettings.NavigationMode) { + this.navigationMode = navigationMode + if (isNavigationMode(BROWSE_FILES)) { + itemCollection.forEach { node -> + node.isSelected = false + } + } + notifyDataSetChanged() + } + + fun renderedCloudNodes(): List> { + return itemCollection + } + + fun selectedCloudNodes(): List> { + return all.filter { it.isSelected } + } + + fun hasUnSelectedNode(): Boolean { + return itemCount > selectedCloudNodes().size + } + + fun filterNodes(nodes: List>?, filterText: String): List>? { + return if (filterText.isNotEmpty()) { + if (sharedPreferencesHandler.useGlobSearch()) { + nodes?.filter { cloudNode -> PatternMatcher(filterText, PatternMatcher.PATTERN_SIMPLE_GLOB).match(cloudNode.name) } + } else { + nodes?.filter { cloudNode -> cloudNode.name.contains(filterText, true) } + } + } else { + nodes + } + } + + // descritto da R.layout.item_gallery_files_node + // sono state importate le sue componenti + // kotlinx.android.synthetic.main.item_gallery_files_node.view.galleryCloudNodeImage + inner class GalleryContentViewHolder internal constructor(itemView: View) : RecyclerViewBaseAdapter<*, *, *>.ItemViewHolder(itemView) { + + private var uiState: UiStateTest? = null + + private var currentProgressIcon: Int = 0 + + private var bound: CloudNodeModel<*>? = null + + override fun bind(position: Int) { + bound = getItem(position) + bound?.let { internalBind(it) } + } + + private fun internalBind(node: CloudNodeModel<*>) { + bindNodeImage(node) + bindLongNodeClick(node) + bindFileOrFolder(node) + } + + private fun bindNodeImage(node: CloudNodeModel<*>) { + if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail != null) { + val bitmap = BitmapFactory.decodeFile(node.thumbnail!!.absolutePath) + itemView.galleryCloudNodeImage.setImageBitmap(bitmap) + } else { + itemView.galleryCloudNodeImage.setImageResource(bindCloudNodeImage(node)) + } + } + + private fun isImageMediaType(filename: String): Boolean { + return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image" + } + + private fun bindCloudNodeImage(cloudNodeModel: CloudNodeModel<*>): Int { + if (cloudNodeModel is CloudFileModel) { + return FileIcon.fileIconFor(cloudNodeModel.name, fileUtil).iconResource + } else if (cloudNodeModel is CloudFolderModel) { + return R.drawable.node_folder + } + throw IllegalStateException("Could not identify the CloudNodeModel type") + } + + private fun bindLongNodeClick(node: CloudNodeModel<*>) { + enableNodeLongClick { + node.isSelected = true + callback.onNodeLongClicked() + true + } + } + + private fun bindFileOrFolder(node: CloudNodeModel<*>) { + if (node is CloudFileModel) { + internalBind(node) + } else { + internalBind(node as CloudFolderModel) + } + } + + private fun internalBind(file: CloudFileModel) { + switchTo(FileDetails()) + bindFile(file) + bindProgressIfPresent(file) + bindSelectItemsModeIfPresent(file) + bindFileSelectionModeIfPresent(file) + } + + private fun bindFile(file: CloudFileModel) { + enableNodeClick { callback.onFileClicked(file) } + } + + private fun bindFileSelectionModeIfPresent(file: CloudFileModel) { + if (isInSelectionMode) { + disableNodeLongClick() + if (!isSelectable(file)) { + itemView.isEnabled = false + } + } + } + + private fun internalBind(folder: CloudFolderModel) { + switchTo(FolderDetails()) + bindFolder(folder) + bindSelectItemsModeIfPresent(folder) + bindFolderSelectionModeIfPresent(folder) + bindProgressIfPresent(folder) + } + + private fun bindSelectItemsModeIfPresent(node: CloudNodeModel<*>) { + if (isNavigationMode(SELECT_ITEMS)) { + if (node is CloudFileModel) { + switchTo(FileSelection()) + } else { + switchTo(FolderSelection()) + } + disableNodeLongClick() +// bindNodeSelection(node) + } + } + + private fun bindProgressIfPresent(node: CloudNodeModel<*>) { + node.progress?.let { showProgress(it) } + } + + private fun bindFolder(folder: CloudFolderModel) { +// itemView.cloudFolderText.text = folder.name + enableNodeClick { callback.onFolderClicked(folder) } + } + + private fun bindFolderSelectionModeIfPresent(folder: CloudFolderModel) { + if (isInSelectionMode) { + disableNodeLongClick() +// hideSettings() + if (!isSelectable(folder)) { + itemView.isEnabled = false + } + } + } + +// private fun bindNodeSelection(cloudNodeModel: CloudNodeModel<*>) { +// itemView.itemCheckBox.setOnCheckedChangeListener { _, isChecked -> +// cloudNodeModel.isSelected = isChecked +// callback.onSelectedNodesChanged(selectedCloudNodes().size) +// } +// enableNodeClick { itemView.itemCheckBox.toggle() } +// +// itemView.itemCheckBox.isChecked = cloudNodeModel.isSelected +// } + + fun showProgress(progress: ProgressModel?) { + bound?.progress = progress + when { + progress?.state() === COMPLETED -> hideProgress() + progress?.progress() == ProgressModel.UNKNOWN_PROGRESS_PERCENTAGE -> showIndeterminateProgress(progress) + progress?.state() !== COMPLETED -> progress?.let { showDeterminateProgress(it) } + } + } + + private fun showIndeterminateProgress(progress: ProgressModel) { + uiState?.let { switchTo(it.indeterminateProgress()) } + if (uiState?.isForFile == true) { +// itemView.cloudFileSubText.setText(progress.state().textResourceId()) + } else { +// itemView.cloudFolderActionText.setText(progress.state().textResourceId()) + } + + if (!progress.state().isSelectable) { + disableNodeActions() + } + } + + private fun disableNodeActions() { + itemView.isEnabled = false +// itemView.settings.visibility = GONE + } + + private fun enableNodeClick(clickListener: View.OnClickListener) { + itemView.setOnClickListener(clickListener) + } + + private fun enableNodeLongClick(longClickListener: View.OnLongClickListener) { + itemView.setOnLongClickListener(longClickListener) + } + + private fun disableNodeLongClick() { + itemView.setOnLongClickListener(null) + } + + private fun showDeterminateProgress(progress: ProgressModel) { + uiState?.let { switchTo(it.determinateProgress()) } + if (uiState?.isForFile == true) { + disableNodeActions() + itemView.cloudFile.progress = progress.progress() + if (currentProgressIcon != progress.state().imageResourceId()) { + currentProgressIcon = progress.state().imageResourceId() + itemView.cloudFileProgress.progressIcon.setImageDrawable(getDrawable(currentProgressIcon)) + } + } else { + // no determinate progress for folders +// itemView.cloudFolderActionText.setText(progress.state().textResourceId()) + } + } + + fun hideProgress() { + uiState?.let { switchTo(it.details()) } + bound?.progress = null + } + + private fun switchTo(state: UiStateTest) { + if (uiState !== state) { + uiState = state + uiState?.apply() + } + } + + fun selectNode(checked: Boolean) { + // TODO: something to show that this photo was successfully selected +// itemView.itemCheckBox.isChecked = checked + } + + abstract inner class UiStateTest(val isForFile: Boolean) { + + fun details(): UiStateTest { + return if (isForFile) { + FileDetails() + } else { + FolderDetails() + } + } + + fun determinateProgress(): UiStateTest { + return if (isForFile) { + FileDeterminateProgress() + } else { + FolderIndeterminateProgress() // no determinate progress for folders + } + } + + fun indeterminateProgress(): UiStateTest { + return if (isForFile) { + FileIndeterminateProgress() + } else { + FolderIndeterminateProgress() + } + } + + abstract fun apply() + } + + inner class FileDetails : UiStateTest(true) { + + override fun apply() { + itemView.isEnabled = true +// itemView.cloudFolderContent.visibility = GONE +// itemView.cloudFileContent.visibility = VISIBLE +// itemView.cloudFileText.visibility = VISIBLE +// itemView.cloudFileSubText.visibility = VISIBLE + itemView.cloudFileProgress.visibility = GONE +// itemView.settings.visibility = VISIBLE +// itemView.itemCheckBox.visibility = GONE + } + } + + inner class FolderDetails : UiStateTest(false) { + + override fun apply() { + itemView.isEnabled = true +// itemView.cloudFileContent.visibility = GONE +// itemView.cloudFolderContent.visibility = VISIBLE +// itemView.cloudFolderText.visibility = VISIBLE +// itemView.cloudFolderActionText.visibility = GONE +// itemView.settings.visibility = VISIBLE +// itemView.itemCheckBox.visibility = GONE + } + } + + inner class FileDeterminateProgress : UiStateTest(true) { + + override fun apply() { +// itemView.cloudFolderContent.visibility = GONE +// itemView.cloudFileContent.visibility = VISIBLE +// itemView.cloudFileText.visibility = VISIBLE +// itemView.cloudFileSubText.visibility = GONE + itemView.cloudFileProgress.visibility = VISIBLE +// itemView.itemCheckBox.visibility = GONE + } + } + + inner class FileIndeterminateProgress : UiStateTest(true) { + + override fun apply() { +// itemView.cloudFolderContent.visibility = GONE +// itemView.cloudFileContent.visibility = VISIBLE +// itemView.cloudFileText.visibility = VISIBLE +// itemView.cloudFileSubText.visibility = VISIBLE + itemView.cloudFileProgress.visibility = GONE +// itemView.itemCheckBox.visibility = GONE + } + + } + + inner class FolderIndeterminateProgress : UiStateTest(false) { + + override fun apply() { +// itemView.cloudFileContent.visibility = GONE +// itemView.cloudFolderContent.visibility = VISIBLE +// itemView.cloudFolderText.visibility = VISIBLE +// itemView.cloudFolderActionText.visibility = VISIBLE +// itemView.itemCheckBox.visibility = GONE + } + } + + inner class FileSelection : UiStateTest(true) { + + override fun apply() { +// itemView.itemCheckBox.visibility = VISIBLE +// itemView.settings.visibility = GONE + } + } + + inner class FolderSelection : UiStateTest(false) { + + override fun apply() { +// itemView.itemCheckBox.visibility = VISIBLE +// itemView.settings.visibility = GONE + } + + } + } + + private fun isSelectable(folder: CloudFolderModel): Boolean { + return chooseCloudNodeSettings?.selectionMode()?.allowsFolders() == true // + && chooseCloudNodeSettings?.excludeFolder(folder) == false + } + + private fun isSelectable(file: CloudFileModel): Boolean { + return chooseCloudNodeSettings?.selectionMode()?.allowsFiles() == true // + && chooseCloudNodeSettings?.namePattern()?.matcher(file.name)?.matches() == true + } + + private fun isNavigationMode(navigationMode: ChooseCloudNodeSettings.NavigationMode): Boolean { + return this.navigationMode == navigationMode + } + + fun setSort(comparator: Comparator>) { + updateComparator(comparator) + } + + interface ItemClickListener { + + fun onFolderClicked(cloudFolderModel: CloudFolderModel) + + fun onFileClicked(cloudNodeModel: CloudFileModel) + + fun onNodeSettingsClicked(cloudNodeModel: CloudNodeModel<*>) + + fun onNodeLongClicked() + + fun onSelectedNodesChanged(selectedNodes: Int) + } + + override fun getSectionName(position: Int): String { + val node = all[position] + + if (node.isFolder) { + return node.name.first().toString() + } + + val formattedModifiedDate = dateHelper.getModifiedDate((node as CloudFileModel).modified) + + return when (comparator) { + is CloudNodeModelDateNewestFirstComparator, is CloudNodeModelDateOldestFirstComparator -> formattedModifiedDate ?: node.name.first().toString() +// is CloudNodeModelSizeBiggestFirstComparator, is CloudNodeModelSizeSmallestFirstComparator -> formattedFileSize ?: node.name.first().toString() + else -> all[position].name.first().toString() + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt new file mode 100644 index 000000000..b35383b6b --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt @@ -0,0 +1,336 @@ +package org.cryptomator.presentation.ui.fragment + +import android.annotation.SuppressLint +import android.os.Bundle +import android.graphics.Rect +import android.view.View +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.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.cryptomator.domain.CloudNode +import org.cryptomator.generator.Fragment +import org.cryptomator.presentation.R +import org.cryptomator.presentation.R.dimen.global_padding +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.NavigationMode.BROWSE_FILES +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.NavigationMode.SELECT_ITEMS +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.SelectionMode.FILES_ONLY +import org.cryptomator.presentation.intent.ChooseCloudNodeSettings.SelectionMode.FOLDERS_ONLY +import org.cryptomator.presentation.model.CloudFileModel +import org.cryptomator.presentation.model.CloudFolderModel +import org.cryptomator.presentation.model.CloudNodeModel +import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.presenter.BrowseFilesPresenter +import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter +import org.cryptomator.presentation.ui.adapter.GalleryFilesAdapter +import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset +import java.util.Optional +import javax.inject.Inject +import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton +import kotlinx.android.synthetic.main.fragment_browse_files.slidingCoordinatorLayout +import kotlinx.android.synthetic.main.fragment_browse_files.swipeRefreshLayout +import kotlinx.android.synthetic.main.recycler_view_layout.recyclerView +import kotlinx.android.synthetic.main.view_browses_files_extra_text_and_button.chooseLocationButton +import kotlinx.android.synthetic.main.view_browses_files_extra_text_and_button.extraText +import kotlinx.android.synthetic.main.view_browses_files_extra_text_and_button.extraTextAndButtonLayout +import kotlinx.android.synthetic.main.view_empty_folder.emptyFolderHint + +@Fragment(R.layout.fragment_gallery_view) +class GalleryFragment : BaseFragment(), FilesFragmentInterface { + + @Inject + lateinit var cloudNodesAdapter: GalleryFilesAdapter + + @Inject + lateinit var browseFilesPresenter: BrowseFilesPresenter + + private var navigationMode: ChooseCloudNodeSettings.NavigationMode? = null + + private var filterText: String = "" + + private final val COLUMNS : Int = 3 + + override var folder: CloudFolderModel + get() = requireArguments().getSerializable(ARG_FOLDER) as CloudFolderModel + set(updatedFolder) { + arguments?.putSerializable(ARG_FOLDER, updatedFolder) + } + + private val chooseCloudNodeSettings: ChooseCloudNodeSettings? + get() = requireArguments().getSerializable(ARG_CHOOSE_CLOUD_NODE_SETTINGS) as ChooseCloudNodeSettings? + + private val refreshListener = SwipeRefreshLayout.OnRefreshListener { browseFilesPresenter.onRefreshTriggered(folder) } + + private val nodeClickListener = object : GalleryFilesAdapter.ItemClickListener { + override fun onFolderClicked(cloudFolderModel: CloudFolderModel) { + browseFilesPresenter.onFolderClicked(cloudFolderModel) + filterText = "" + browseFilesPresenter.invalidateOptionsMenu() + } + + override fun onFileClicked(cloudNodeModel: CloudFileModel) { + if (fileCanBeChosen(cloudNodeModel)) { + browseFilesPresenter.onFileChosen(cloudNodeModel) + } else { + browseFilesPresenter.onFileClicked(cloudNodeModel) + } + } + + override fun onNodeSettingsClicked(cloudNodeModel: CloudNodeModel<*>) { + browseFilesPresenter.onNodeSettingsClicked(cloudNodeModel) + } + + override fun onNodeLongClicked() { + browseFilesPresenter.onSelectionModeActivated() + } + + override fun onSelectedNodesChanged(selectedNodes: Int) { + browseFilesPresenter.onSelectedNodesChanged(selectedNodes) + } + } + + override val selectedCloudNodes: List> + get() = cloudNodesAdapter.selectedCloudNodes() + + override fun setupView() { + setupNavigationMode() + + floatingActionButton.setOnClickListener { browseFilesPresenter.onAddContentClicked() } + chooseLocationButton.setOnClickListener { browseFilesPresenter.onFolderChosen(folder) } + + swipeRefreshLayout.setColorSchemeColors(ContextCompat.getColor(context(), R.color.colorPrimary)) + swipeRefreshLayout.setOnRefreshListener(refreshListener) + + cloudNodesAdapter.setCallback(nodeClickListener) + cloudNodesAdapter.setChooseCloudNodeSettings(chooseCloudNodeSettings) + navigationMode?.let { cloudNodesAdapter.updateNavigationMode(it) } + + + recyclerView.layoutManager = GridLayoutManager(context(), COLUMNS) + recyclerView.adapter = cloudNodesAdapter + recyclerView.setHasFixedSize(true) + + val spacing = getResources().getDimensionPixelSize(R.dimen.global_padding) / 4 + + // bottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 88f, resources.displayMetrics).toInt() + recyclerView.setPadding(spacing, spacing, spacing, spacing) + recyclerView.clipToPadding = false + recyclerView.clipChildren = false + + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) { + outRect.set(spacing, spacing, spacing, spacing) + } + }) + + browseFilesPresenter.onFolderRedisplayed(folder) + + when { + !hasCloudNodeSettings() -> setupViewForBrowseFilesMode() + isSelectionMode(FOLDERS_ONLY) -> setupViewForFolderSelection() + isSelectionMode(FILES_ONLY) -> setupViewForFilesSelection() + isNavigationMode(SELECT_ITEMS) -> setupViewForNodeSelectionMode() + } + } + + private fun isNavigationMode(navigationMode: ChooseCloudNodeSettings.NavigationMode): Boolean = this.navigationMode == navigationMode + + private fun setupNavigationMode() { + navigationMode = if (hasCloudNodeSettings()) { + chooseCloudNodeSettings?.navigationMode() + } else { + BROWSE_FILES + } + } + + private fun setupViewForBrowseFilesMode() { + showFloatingActionButton() + swipeRefreshLayout.isEnabled = true + } + + private fun setupViewForNodeSelectionMode() { + hideFloatingActionButton() + disableSwipeRefresh() + } + + private fun disableSwipeRefresh() { + swipeRefreshLayout.isRefreshing = false + swipeRefreshLayout.isEnabled = false + } + + private fun setupViewForFilesSelection() { + extraTextAndButtonLayout.visibility = VISIBLE + chooseLocationButton.visibility = GONE + extraText.text = chooseCloudNodeSettings?.extraText() + val layoutParams = extraText.layoutParams as RelativeLayout.LayoutParams + layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL) + extraText?.layoutParams = layoutParams + disableSwipeRefresh() + } + + private fun setupViewForFolderSelection() { + extraTextAndButtonLayout?.visibility = VISIBLE + chooseLocationButton.visibility = VISIBLE + chooseLocationButton.text = chooseCloudNodeSettings?.buttonText() + extraText.text = chooseCloudNodeSettings?.extraText() + extraText.setPadding(getPixelOffset(global_padding), 0, 0, 0) + disableSwipeRefresh() + } + + @SuppressLint("RestrictedApi") // Due to bug https://stackoverflow.com/questions/50343634/android-p-visibilityawareimagebutton-setvisibility-can-only-be-called-from-the-s + private fun showFloatingActionButton() { + floatingActionButton.visibility = VISIBLE + } + + @SuppressLint("RestrictedApi") // Due to bug https://stackoverflow.com/questions/50343634/android-p-visibilityawareimagebutton-setvisibility-can-only-be-called-from-the-s + private fun hideFloatingActionButton() { + floatingActionButton.visibility = GONE + } + + override fun loadContent() { + browseFilesPresenter.onFolderDisplayed(folder) + } + + override fun loadContentSilent() { + browseFilesPresenter.onFolderReloadContent(folder) + } + + override fun show(nodes: List>?) { + cloudNodesAdapter.clear() + cloudNodesAdapter.addAll(cloudNodesAdapter.filterNodes(nodes, filterText)) + updateEmptyFolderHint() + } + + override fun showProgress(nodes: List>?, progress: ProgressModel?) { + nodes?.forEach { node -> + showProgress(node, progress) + } + } + + override fun showProgress(node: CloudNodeModel<*>?, progress: ProgressModel?) { + val viewHolder = viewHolderFor(node) + if (viewHolder.isPresent) { + viewHolder.get().showProgress(progress) + } else { + node?.progress = progress + node?.let { addOrUpdate(it) } + } + } + + override fun hideProgress(nodes: List>?) { + nodes?.forEach { node -> + hideProgress(node) + } + } + + override fun hideProgress(cloudNode: CloudNodeModel<*>?) { + val viewHolder = viewHolderFor(cloudNode) + if (viewHolder.isPresent) { + viewHolder.get().hideProgress() + } else { + cloudNode?.progress = ProgressModel.COMPLETED + cloudNode?.let { addOrUpdate(it) } + } + } + + override fun selectAllItems() { + val hasUnSelectedNode = cloudNodesAdapter.hasUnSelectedNode() + cloudNodesAdapter.renderedCloudNodes().forEach { node -> + selectNode(node, hasUnSelectedNode) + } + } + + private fun selectNode(node: CloudNodeModel<*>, selected: Boolean) { + val viewHolder = viewHolderFor(node) + if (viewHolder.isPresent) { + viewHolder.get().selectNode(selected) + } else { + node.isSelected = selected + addOrUpdate(node) + } + } + + override fun remove(cloudNode: List>?) { + cloudNodesAdapter.deleteItems(cloudNode) + updateEmptyFolderHint() + } + + private fun viewHolderFor(nodeModel: CloudNodeModel<*>?): Optional { + val positionOf = cloudNodesAdapter.positionOf(nodeModel) + return Optional.ofNullable(recyclerView.findViewHolderForAdapterPosition(positionOf) as? BrowseFilesAdapter.VaultContentViewHolder) + } + + override fun replaceRenamedCloudFile(cloudFile: CloudNodeModel) { + cloudNodesAdapter.replaceRenamedCloudFile(cloudFile) + } + + override fun showLoading(loading: Boolean?) { + loading?.let { swipeRefreshLayout.isRefreshing = it } + } + + override fun addOrUpdate(cloudNode: CloudNodeModel<*>) { + cloudNodesAdapter.addOrReplaceCloudNode(cloudNode) + updateEmptyFolderHint() + } + + private fun updateEmptyFolderHint() { + emptyFolderHint.visibility = if (cloudNodesAdapter.isEmpty) VISIBLE else GONE + } + + private fun fileCanBeChosen(cloudFile: CloudFileModel): Boolean { + val settings = chooseCloudNodeSettings + return settings != null && settings.selectionMode().allowsFiles() && settings.namePattern().matcher(cloudFile.name).matches() + } + + private fun hasCloudNodeSettings(): Boolean = chooseCloudNodeSettings != null + + private fun isSelectionMode(selectionMode: ChooseCloudNodeSettings.SelectionMode): + Boolean = chooseCloudNodeSettings?.selectionMode() == selectionMode + + override fun renderedCloudNodes(): List> = cloudNodesAdapter.renderedCloudNodes() + + override fun rootView(): View = slidingCoordinatorLayout + + override fun navigationModeChanged(navigationMode: ChooseCloudNodeSettings.NavigationMode) { + updateNavigationMode(navigationMode) + + if (navigationMode == SELECT_ITEMS) { + setupViewForNodeSelectionMode() + } else if (navigationMode == BROWSE_FILES) { + setupViewForBrowseFilesMode() + } + } + + private fun updateNavigationMode(navigationMode: ChooseCloudNodeSettings.NavigationMode) { + this.navigationMode = navigationMode + cloudNodesAdapter.updateNavigationMode(navigationMode) + } + + override fun setFilterText(query: String) { + filterText = query + } + + override fun setSort(comparator: Comparator>) { + cloudNodesAdapter.setSort(comparator) + } + + companion object { + + private const val ARG_FOLDER = "folder" + private const val ARG_CHOOSE_CLOUD_NODE_SETTINGS = "chooseCloudNodeSettings" + + fun newInstance(folder: CloudFolderModel, chooseCloudNodeSettings: ChooseCloudNodeSettings?): GalleryFragment { + val result = GalleryFragment() + val args = Bundle() + args.putSerializable(ARG_FOLDER, folder) + args.putSerializable(ARG_CHOOSE_CLOUD_NODE_SETTINGS, chooseCloudNodeSettings) + result.arguments = args + return result + } + } + +} diff --git a/presentation/src/main/res/layout/fragment_gallery_view.xml b/presentation/src/main/res/layout/fragment_gallery_view.xml new file mode 100644 index 000000000..469d935b7 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_gallery_view.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/item_gallery_files_node.xml b/presentation/src/main/res/layout/item_gallery_files_node.xml new file mode 100644 index 000000000..23748641d --- /dev/null +++ b/presentation/src/main/res/layout/item_gallery_files_node.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From 845c6707067805574379a1b306d0a22236db6b1f Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Fri, 3 May 2024 01:15:25 +0200 Subject: [PATCH 20/30] Select the correct Fragment based on UploadVault --- .../ui/activity/BrowseFilesActivity.kt | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 2a3c30c1a..041272287 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -49,6 +49,8 @@ import org.cryptomator.presentation.ui.dialog.ReplaceDialog import org.cryptomator.presentation.ui.dialog.SymLinkDialog import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment +import org.cryptomator.presentation.ui.fragment.FilesFragmentInterface +import org.cryptomator.presentation.ui.fragment.GalleryFragment import java.util.regex.Pattern import javax.inject.Inject import kotlinx.android.synthetic.main.toolbar_layout.toolbar @@ -103,10 +105,7 @@ class BrowseFilesActivity : BaseActivity(), // get() = browseFilesFragment().folder override fun createFragment(): Fragment = - BrowseFilesFragment.newInstance( - browseFilesIntent.folder(), - browseFilesIntent.chooseCloudNodeSettings() - ) + createFragmentFor(browseFilesIntent.folder(), browseFilesIntent.chooseCloudNodeSettings()) override fun onDestroy() { super.onDestroy() @@ -422,14 +421,22 @@ class BrowseFilesActivity : BaseActivity(), // override fun navigateTo(folder: CloudFolderModel) { replaceFragment( - BrowseFilesFragment.newInstance( - folder, - browseFilesIntent.chooseCloudNodeSettings() - ), + createFragmentFor(folder), FragmentAnimation.NAVIGATE_IN_TO_FOLDER ) } + private fun createFragmentFor(folder: CloudFolderModel) : Fragment { + return createFragmentFor(folder, browseFilesIntent.chooseCloudNodeSettings()) + } + private fun createFragmentFor(folder: CloudFolderModel, chooseCloudNodeSettings : ChooseCloudNodeSettings?) : Fragment { + return if(folder.path == sharedPreferencesHandler.photoUploadVaultFolder()) { + GalleryFragment.newInstance(folder, chooseCloudNodeSettings) + } else { + BrowseFilesFragment.newInstance(folder, chooseCloudNodeSettings) + } + } + override fun showAddContentDialog() { VaultContentActionBottomSheet.newInstance(browseFilesFragment().folder) .show(supportFragmentManager, "AddContentDialog") @@ -512,10 +519,7 @@ class BrowseFilesActivity : BaseActivity(), // private fun createBackStackFor(sourceParent: CloudFolderModel) { replaceFragment( - BrowseFilesFragment.newInstance( - sourceParent, - browseFilesIntent.chooseCloudNodeSettings() - ), + createFragmentFor(sourceParent), FragmentAnimation.NAVIGATE_OUT_OF_FOLDER, false ) @@ -553,7 +557,7 @@ class BrowseFilesActivity : BaseActivity(), // browseFilesFragment().showLoading(loading) } - private fun browseFilesFragment(): BrowseFilesFragment = getCurrentFragment(R.id.fragmentContainer) as BrowseFilesFragment + private fun browseFilesFragment(): FilesFragmentInterface = getCurrentFragment(R.id.fragmentContainer) as FilesFragmentInterface override fun onCreateNewTextFileClicked(fileName: String) { browseFilesPresenter.onCreateNewTextFileClicked(browseFilesFragment().folder, fileName) From b461c13dcedb38c1a84aefba58258a9ff3afe186 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Fri, 3 May 2024 01:17:15 +0200 Subject: [PATCH 21/30] Use custom format date in the FastScroll --- .../org/cryptomator/presentation/util/DateHelper.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt index 575c57430..29bd5a776 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt @@ -1,12 +1,18 @@ package org.cryptomator.presentation.util import org.cryptomator.presentation.R +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject class DateHelper @Inject constructor() { + private val dateFormatter by lazy { + SimpleDateFormat("MM/yyyy") + } + fun getFormattedModifiedDate(modified: Date?): String? { return modified?.let { val modifiedAgo = currentDate().time - it.time @@ -14,6 +20,12 @@ class DateHelper @Inject constructor() { } } + fun getModifiedDate(modified: Date?): String? { + return modified?.let { + dateFormatter.format(it) + } + } + private fun convert(time: Long): String { return DurationHandler.values() .firstOrNull { it.isApplicable(time) } From 4a9854fdc8f70ea9bb66e2c18f18a91767e70e1f Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 4 May 2024 01:59:43 +0200 Subject: [PATCH 22/30] Add vault.id in the BrowseFilesIntent --- .../cryptomator/presentation/intent/BrowseFilesIntent.java | 2 ++ .../presentation/presenter/AutoUploadChooseVaultPresenter.kt | 1 + .../presentation/presenter/BrowseFilesPresenter.kt | 5 ++++- .../presentation/presenter/SharedFilesPresenter.kt | 1 + .../presentation/ui/activity/VaultListActivity.kt | 2 +- 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/intent/BrowseFilesIntent.java b/presentation/src/main/java/org/cryptomator/presentation/intent/BrowseFilesIntent.java index a38712995..075b16c51 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/intent/BrowseFilesIntent.java +++ b/presentation/src/main/java/org/cryptomator/presentation/intent/BrowseFilesIntent.java @@ -12,6 +12,8 @@ public interface BrowseFilesIntent { @Optional String title(); + @Optional + Long vaultId(); @Optional ChooseCloudNodeSettings chooseCloudNodeSettings(); diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt index 1f9adce4f..7ed7a7225 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt @@ -141,6 +141,7 @@ class AutoUploadChooseVaultPresenter @Inject constructor( // requestActivityResult( // ActivityResultCallbacks.onAutoUploadChooseLocation(vaultModel), // Intents.browseFilesIntent() // + .withVaultId(vaultModel.vaultId) // .withFolder(decryptedRoot) // .withTitle(vaultModel.name) // .withChooseCloudNodeSettings( // 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..b114d5660 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt @@ -1099,7 +1099,8 @@ class BrowseFilesPresenter @Inject constructor( // private fun moveIntentFor(parent: CloudFolderModel, sourceNodes: List>): IntentBuilder { val foldersToMove = nodesFor(sourceNodes, CloudFolderModel::class) as List - return Intents.browseFilesIntent() // + val vauldId = view?.folder?.vault()?.vaultId + val browseFilesIntentBuilder = Intents.browseFilesIntent() // .withTitle(effectiveMoveTitle()) // .withFolder(parent) // .withChooseCloudNodeSettings( // @@ -1112,6 +1113,8 @@ class BrowseFilesPresenter @Inject constructor( // .excludingFolder(if (foldersToMove.isEmpty()) null else foldersToMove) // .build() ) + vauldId?.let { browseFilesIntentBuilder.withVaultId(it) } + return browseFilesIntentBuilder } private fun effectiveMoveTitle(): String { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt index b9c87bf3a..4d5bb204f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt @@ -343,6 +343,7 @@ class SharedFilesPresenter @Inject constructor( // requestActivityResult( // ActivityResultCallbacks.onChooseLocation(vaultModel), // Intents.browseFilesIntent() // + .withVaultId(vaultModel.vaultId) // .withFolder(decryptedRoot) // .withTitle(vaultModel.name) // .withChooseCloudNodeSettings( // diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt index 7fe53a3b6..450570a7a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt @@ -153,7 +153,7 @@ class VaultListActivity : BaseActivity(), // } override fun navigateToVaultContent(vault: VaultModel, decryptedRoot: CloudFolderModel) { - vaultListPresenter.startIntent(browseFilesIntent().withTitle(vault.name).withFolder(decryptedRoot)) + vaultListPresenter.startIntent(browseFilesIntent().withVaultId(vault.vaultId).withTitle(vault.name).withFolder(decryptedRoot)) } override fun renameVault(vaultModel: VaultModel) { From ac6aa7b0284b1232e3144f0b88304b8acb4a90eb Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 4 May 2024 02:02:19 +0200 Subject: [PATCH 23/30] Check vault.id and folder ('auto-upload') for GalleryFragment usage --- .../presentation/ui/activity/BrowseFilesActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 041272287..701478e4d 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -430,13 +430,17 @@ class BrowseFilesActivity : BaseActivity(), // return createFragmentFor(folder, browseFilesIntent.chooseCloudNodeSettings()) } private fun createFragmentFor(folder: CloudFolderModel, chooseCloudNodeSettings : ChooseCloudNodeSettings?) : Fragment { - return if(folder.path == sharedPreferencesHandler.photoUploadVaultFolder()) { + return if(isAutoUploadFolder(browseFilesIntent.vaultId(), folder.path)) { GalleryFragment.newInstance(folder, chooseCloudNodeSettings) } else { BrowseFilesFragment.newInstance(folder, chooseCloudNodeSettings) } } + private fun isAutoUploadFolder(vaultId : Long, folderPath : String) : Boolean { + return vaultId == sharedPreferencesHandler.photoUploadVault() && folderPath == sharedPreferencesHandler.photoUploadVaultFolder() + } + override fun showAddContentDialog() { VaultContentActionBottomSheet.newInstance(browseFilesFragment().folder) .show(supportFragmentManager, "AddContentDialog") From 56f3a0d9721db04a0200eb9445ad98a62020bd3a Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 4 May 2024 02:07:04 +0200 Subject: [PATCH 24/30] Add rectangle frame for image selection in GalleryFragment --- .../ui/adapter/GalleryFilesAdapter.kt | 47 +++++++++++++++---- .../res/drawable/rectangle_selection_mode.xml | 7 +++ .../res/layout/item_gallery_files_node.xml | 1 + presentation/src/main/res/values/colors.xml | 1 + 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 presentation/src/main/res/drawable/rectangle_selection_mode.xml diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt index b18fa9fd6..4d860aca3 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt @@ -32,6 +32,7 @@ import org.cryptomator.util.file.MimeTypes import javax.inject.Inject import kotlinx.android.synthetic.main.item_gallery_files_node.view.cloudFileProgress import kotlinx.android.synthetic.main.item_gallery_files_node.view.galleryCloudNodeImage +import kotlinx.android.synthetic.main.item_gallery_files_node.view.galleryItemContainer import kotlinx.android.synthetic.main.item_gallery_files_node.view.progressIcon import kotlinx.android.synthetic.main.view_cloud_file_progress.view.cloudFile @@ -215,7 +216,7 @@ constructor( switchTo(FolderSelection()) } disableNodeLongClick() -// bindNodeSelection(node) + bindNodeSelection(node) } } @@ -237,16 +238,42 @@ constructor( } } } + private fun bindNodeSelection(cloudNodeModel: CloudNodeModel<*>) { + itemView.galleryItemContainer.setOnLongClickListener { /* https://stackoverflow.com/a/12230526 + As you may know, the View hierarchy in Android is represented by a tree. + When you return true from the onItemLongClick() - it means that the View that + currently received the event is the true event receiver and the event should + not be propagated to the other Views in the tree; when you return false - + you let the event be passed to the other Views that may consume it. + */ + toggleSelection(cloudNodeModel) + true + } + + enableNodeClick{ + toggleSelection(cloudNodeModel) + } + + // first set + if (cloudNodeModel.isSelected) { + itemView.galleryItemContainer.foreground = getDrawable(R.drawable.rectangle_selection_mode) + callback.onSelectedNodesChanged(selectedCloudNodes().size) + } + } + + private fun toggleSelection(cloudNodeModel : CloudNodeModel<*>) { + // toggle selection + cloudNodeModel.isSelected = !cloudNodeModel.isSelected -// private fun bindNodeSelection(cloudNodeModel: CloudNodeModel<*>) { -// itemView.itemCheckBox.setOnCheckedChangeListener { _, isChecked -> -// cloudNodeModel.isSelected = isChecked -// callback.onSelectedNodesChanged(selectedCloudNodes().size) -// } -// enableNodeClick { itemView.itemCheckBox.toggle() } -// -// itemView.itemCheckBox.isChecked = cloudNodeModel.isSelected -// } + // toggle rectangle + if (itemView.galleryItemContainer.foreground == null) + itemView.galleryItemContainer.foreground = getDrawable(R.drawable.rectangle_selection_mode) + else + itemView.galleryItemContainer.foreground = null + + // update screen info + callback.onSelectedNodesChanged(selectedCloudNodes().size) + } fun showProgress(progress: ProgressModel?) { bound?.progress = progress diff --git a/presentation/src/main/res/drawable/rectangle_selection_mode.xml b/presentation/src/main/res/drawable/rectangle_selection_mode.xml new file mode 100644 index 000000000..b8c5648c6 --- /dev/null +++ b/presentation/src/main/res/drawable/rectangle_selection_mode.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_gallery_files_node.xml b/presentation/src/main/res/layout/item_gallery_files_node.xml index 23748641d..2a469b0df 100644 --- a/presentation/src/main/res/layout/item_gallery_files_node.xml +++ b/presentation/src/main/res/layout/item_gallery_files_node.xml @@ -3,6 +3,7 @@ #49B04A + #4D49B04A #66CC68 #407F41 From 2135ecb9c389fe9115ce5234623088c53f8fd939 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sat, 4 May 2024 02:08:49 +0200 Subject: [PATCH 25/30] Re-insert file size comparator for FastScrollRecyclerView --- .../presentation/ui/adapter/GalleryFilesAdapter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt index 4d860aca3..682161c19 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt @@ -494,11 +494,13 @@ constructor( return node.name.first().toString() } - val formattedModifiedDate = dateHelper.getModifiedDate((node as CloudFileModel).modified) + node as CloudFileModel + val formattedFileSize = fileSizeHelper.getFormattedFileSize((node).size) + val formattedModifiedDate = dateHelper.getModifiedDate((node).modified) return when (comparator) { is CloudNodeModelDateNewestFirstComparator, is CloudNodeModelDateOldestFirstComparator -> formattedModifiedDate ?: node.name.first().toString() -// is CloudNodeModelSizeBiggestFirstComparator, is CloudNodeModelSizeSmallestFirstComparator -> formattedFileSize ?: node.name.first().toString() + is CloudNodeModelSizeBiggestFirstComparator, is CloudNodeModelSizeSmallestFirstComparator -> formattedFileSize ?: node.name.first().toString() else -> all[position].name.first().toString() } } From 16438f549017ac200956d34d15a71e742e8f1283 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 5 May 2024 00:16:59 +0200 Subject: [PATCH 26/30] Change date format used in FastScroll --- .../main/java/org/cryptomator/presentation/util/DateHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt index 29bd5a776..589005986 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/DateHelper.kt @@ -10,7 +10,7 @@ import javax.inject.Inject class DateHelper @Inject constructor() { private val dateFormatter by lazy { - SimpleDateFormat("MM/yyyy") + SimpleDateFormat("yyyy/MM/dd - HH:mm") } fun getFormattedModifiedDate(modified: Date?): String? { From 84d617e0b10953f993f006b8158f9fcf1e9bf0f0 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 5 May 2024 01:53:50 +0200 Subject: [PATCH 27/30] Fix correct recycle of ViewHolder When onBackPressed(), or similar, were triggered the recycle/bind reused a ViewHolder with the selection mode already set --- .../ui/adapter/GalleryFilesAdapter.kt | 50 +++++++++++++------ .../ui/fragment/GalleryFragment.kt | 10 ++-- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt index 682161c19..82d401586 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/GalleryFilesAdapter.kt @@ -67,6 +67,10 @@ constructor( } } + fun triggerUpdateSelectedNodesNumberInfo() { + callback.onSelectedNodesChanged(selectedCloudNodes().size) + } + fun replaceRenamedCloudFile(cloudNode: CloudNodeModel) { itemCollection.forEach { nodes -> if (nodes.javaClass == cloudNode.javaClass && nodes.name == cloudNode.oldName) { @@ -136,11 +140,20 @@ constructor( } private fun internalBind(node: CloudNodeModel<*>) { + clearPreviousHolderSelection() bindNodeImage(node) bindLongNodeClick(node) bindFileOrFolder(node) } + private fun clearPreviousHolderSelection() { + // durante il rebind sta probabilmente riutilizzando lo stesso oggetto grafico (itemView) + // di un precente cloudNode che era stato selezionato + // e.g. se l'item 22 viene selezionato, cambia il foreground e quando viene + // ribindato con l'indice 0 rimane il foregound sbagliato! + itemView.galleryItemContainer.foreground = null + } + private fun bindNodeImage(node: CloudNodeModel<*>) { if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail != null) { val bitmap = BitmapFactory.decodeFile(node.thumbnail!!.absolutePath) @@ -239,16 +252,18 @@ constructor( } } private fun bindNodeSelection(cloudNodeModel: CloudNodeModel<*>) { - itemView.galleryItemContainer.setOnLongClickListener { /* https://stackoverflow.com/a/12230526 - As you may know, the View hierarchy in Android is represented by a tree. - When you return true from the onItemLongClick() - it means that the View that - currently received the event is the true event receiver and the event should - not be propagated to the other Views in the tree; when you return false - - you let the event be passed to the other Views that may consume it. - */ - toggleSelection(cloudNodeModel) - true - } + // this method is invoked for each item to be displayed! + +// itemView.galleryItemContainer.setOnLongClickListener { /* https://stackoverflow.com/a/12230526 +// As you may know, the View hierarchy in Android is represented by a tree. +// When you return true from the onItemLongClick() - it means that the View that +// currently received the event is the true event receiver and the event should +// not be propagated to the other Views in the tree; when you return false - +// you let the event be passed to the other Views that may consume it. +// */ +// toggleSelection(cloudNodeModel) +// true +// } enableNodeClick{ toggleSelection(cloudNodeModel) @@ -257,7 +272,7 @@ constructor( // first set if (cloudNodeModel.isSelected) { itemView.galleryItemContainer.foreground = getDrawable(R.drawable.rectangle_selection_mode) - callback.onSelectedNodesChanged(selectedCloudNodes().size) + triggerUpdateSelectedNodesNumberInfo() } } @@ -266,13 +281,13 @@ constructor( cloudNodeModel.isSelected = !cloudNodeModel.isSelected // toggle rectangle - if (itemView.galleryItemContainer.foreground == null) + if (cloudNodeModel.isSelected) itemView.galleryItemContainer.foreground = getDrawable(R.drawable.rectangle_selection_mode) else itemView.galleryItemContainer.foreground = null // update screen info - callback.onSelectedNodesChanged(selectedCloudNodes().size) + triggerUpdateSelectedNodesNumberInfo() } fun showProgress(progress: ProgressModel?) { @@ -342,8 +357,13 @@ constructor( } fun selectNode(checked: Boolean) { - // TODO: something to show that this photo was successfully selected -// itemView.itemCheckBox.isChecked = checked + if (checked) + itemView.galleryItemContainer.foreground = getDrawable(R.drawable.rectangle_selection_mode) + else + itemView.galleryItemContainer.foreground = null + + bound?.let { it.isSelected = checked } + triggerUpdateSelectedNodesNumberInfo() } abstract inner class UiStateTest(val isForFile: Boolean) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt index b35383b6b..a90618485 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/GalleryFragment.kt @@ -25,7 +25,6 @@ import org.cryptomator.presentation.model.CloudFolderModel import org.cryptomator.presentation.model.CloudNodeModel import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.presenter.BrowseFilesPresenter -import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter import org.cryptomator.presentation.ui.adapter.GalleryFilesAdapter import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset import java.util.Optional @@ -52,7 +51,7 @@ class GalleryFragment : BaseFragment(), FilesFragmentInterface { private var filterText: String = "" - private final val COLUMNS : Int = 3 + private val COLUMNS : Int = 3 override var folder: CloudFolderModel get() = requireArguments().getSerializable(ARG_FOLDER) as CloudFolderModel @@ -114,7 +113,7 @@ class GalleryFragment : BaseFragment(), FilesFragmentInterface { recyclerView.adapter = cloudNodesAdapter recyclerView.setHasFixedSize(true) - val spacing = getResources().getDimensionPixelSize(R.dimen.global_padding) / 4 + val spacing = resources.getDimensionPixelSize(global_padding) / 4 // bottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 88f, resources.displayMetrics).toInt() recyclerView.setPadding(spacing, spacing, spacing, spacing) @@ -252,6 +251,7 @@ class GalleryFragment : BaseFragment(), FilesFragmentInterface { node.isSelected = selected addOrUpdate(node) } + cloudNodesAdapter.triggerUpdateSelectedNodesNumberInfo() } override fun remove(cloudNode: List>?) { @@ -259,9 +259,9 @@ class GalleryFragment : BaseFragment(), FilesFragmentInterface { updateEmptyFolderHint() } - private fun viewHolderFor(nodeModel: CloudNodeModel<*>?): Optional { + private fun viewHolderFor(nodeModel: CloudNodeModel<*>?): Optional { val positionOf = cloudNodesAdapter.positionOf(nodeModel) - return Optional.ofNullable(recyclerView.findViewHolderForAdapterPosition(positionOf) as? BrowseFilesAdapter.VaultContentViewHolder) + return Optional.ofNullable(recyclerView.findViewHolderForAdapterPosition(positionOf) as? GalleryFilesAdapter.GalleryContentViewHolder) } override fun replaceRenamedCloudFile(cloudFile: CloudNodeModel) { From e3085b573d561ce6bdd34cbad723dcc6169b742b Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 5 May 2024 03:37:47 +0200 Subject: [PATCH 28/30] Add null check on vault.id in createFragmentFor --- .../presentation/ui/activity/BrowseFilesActivity.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 701478e4d..3cd61b135 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -430,11 +430,12 @@ class BrowseFilesActivity : BaseActivity(), // return createFragmentFor(folder, browseFilesIntent.chooseCloudNodeSettings()) } private fun createFragmentFor(folder: CloudFolderModel, chooseCloudNodeSettings : ChooseCloudNodeSettings?) : Fragment { - return if(isAutoUploadFolder(browseFilesIntent.vaultId(), folder.path)) { - GalleryFragment.newInstance(folder, chooseCloudNodeSettings) - } else { - BrowseFilesFragment.newInstance(folder, chooseCloudNodeSettings) + browseFilesIntent.vaultId()?.let { id -> + if(isAutoUploadFolder(id, folder.path)) { + return GalleryFragment.newInstance(folder, chooseCloudNodeSettings) + } } + return BrowseFilesFragment.newInstance(folder, chooseCloudNodeSettings) } private fun isAutoUploadFolder(vaultId : Long, folderPath : String) : Boolean { From fa224a4d7d18f43b82431a2f607ddd72c4095b32 Mon Sep 17 00:00:00 2001 From: Luca Fantini Date: Sun, 5 May 2024 03:39:36 +0200 Subject: [PATCH 29/30] Set ThumbnailGeneration option default as NEVER --- presentation/src/main/res/xml/preferences.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 217fb5536..e1d6c82d5 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -123,7 +123,7 @@ Date: Wed, 8 May 2024 16:09:33 +0200 Subject: [PATCH 30/30] Thumbnail dim --- .../org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 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 fbf4300e2..f8aeb5711 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 @@ -439,8 +439,8 @@ abstract class CryptoImplDecorator( 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 + val thumbnailWidth = 175 + val thumbnailHeight = 175 thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight) if (thumbnailBitmap != null) {