From fd5ddc02b19bc370cc476a29b5a5be8caa5ec162 Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Thu, 11 Nov 2021 15:05:33 +0100 Subject: [PATCH] Thumbnails --- .../sushi/hardcore/droidfs/CameraActivity.kt | 2 +- .../sushi/hardcore/droidfs/ConstValues.kt | 9 -- .../sushi/hardcore/droidfs/CreateActivity.kt | 2 +- .../sushi/hardcore/droidfs/GocryptfsVolume.kt | 64 +++++++-- .../sushi/hardcore/droidfs/OpenActivity.kt | 4 +- .../adapters/ExplorerElementAdapter.kt | 136 +++++++++++------- .../droidfs/explorers/BaseExplorerActivity.kt | 13 +- .../droidfs/explorers/ExplorerActivity.kt | 2 +- .../droidfs/explorers/ExplorerActivityPick.kt | 3 +- .../file_viewers/FileViewerActivity.kt | 53 ++----- app/src/main/res/drawable/icon_image.xml | 5 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/root_preferences.xml | 7 + 13 files changed, 180 insertions(+), 122 deletions(-) create mode 100644 app/src/main/res/drawable/icon_image.xml diff --git a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt index dc38516..158d94f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt @@ -91,7 +91,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) binding = ActivityCameraBinding.inflate(layoutInflater) setContentView(binding.root) - gocryptfsVolume = GocryptfsVolume(intent.getIntExtra("sessionID", -1)) + gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1)) outputDirectory = intent.getStringExtra("path")!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { diff --git a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt index 65fe7b9..16654e0 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt @@ -38,14 +38,5 @@ class ConstValues { fun isText(path: String): Boolean { return isExtensionType("text", path) } - fun getAssociatedDrawable(path: String): Int { - return when { - isAudio(path) -> R.drawable.icon_file_audio - isImage(path) -> R.drawable.icon_file_image - isVideo(path) -> R.drawable.icon_file_video - isText(path) -> R.drawable.icon_file_text - else -> R.drawable.icon_file_unknown - } - } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt index 9b8a154..0fe685f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt @@ -167,7 +167,7 @@ class CreateActivity : VolumeActionActivity() { super.onPause() //Closing volume if leaving activity while showing dialog if (sessionID != -1 && !isStartingExplorer) { - GocryptfsVolume(sessionID).close() + GocryptfsVolume(applicationContext, sessionID).close() finish() } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt index 93242da..4a1f0eb 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt @@ -9,7 +9,7 @@ import java.io.FileOutputStream import java.io.InputStream import java.io.OutputStream -class GocryptfsVolume(var sessionID: Int) { +class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) { private external fun native_close(sessionID: Int) private external fun native_is_closed(sessionID: Int): Boolean private external fun native_list_dir(sessionID: Int, dir_path: String): MutableList @@ -47,79 +47,79 @@ class GocryptfsVolume(var sessionID: Int) { } fun close() { - synchronized(this){ + synchronized(applicationContext) { native_close(sessionID) } } fun isClosed(): Boolean { - synchronized(this){ + synchronized(applicationContext) { return native_is_closed(sessionID) } } fun listDir(dir_path: String): MutableList { - synchronized(this){ + synchronized(applicationContext) { return native_list_dir(sessionID, dir_path) } } fun mkdir(dir_path: String): Boolean { - synchronized(this){ + synchronized(applicationContext) { return native_mkdir(sessionID, dir_path, ConstValues.DIRECTORY_MODE) } } fun rmdir(dir_path: String): Boolean { - synchronized(this){ + synchronized(applicationContext) { return native_rmdir(sessionID, dir_path) } } fun removeFile(file_path: String): Boolean { - synchronized(this){ + synchronized(applicationContext) { return native_remove_file(sessionID, file_path) } } fun pathExists(file_path: String): Boolean { - synchronized(this){ + synchronized(applicationContext) { return native_path_exists(sessionID, file_path) } } fun getSize(file_path: String): Long { - synchronized(this){ + synchronized(applicationContext) { return native_get_size(sessionID, file_path) } } fun closeFile(handleID: Int) { - synchronized(this){ + synchronized(applicationContext) { native_close_file(sessionID, handleID) } } fun openReadMode(file_path: String): Int { - synchronized(this){ + synchronized(applicationContext) { return native_open_read_mode(sessionID, file_path) } } fun openWriteMode(file_path: String): Int { - synchronized(this){ + synchronized(applicationContext) { return native_open_write_mode(sessionID, file_path, ConstValues.FILE_MODE) } } fun readFile(handleID: Int, offset: Long, buff: ByteArray): Int { - synchronized(this){ + synchronized(applicationContext) { return native_read_file(sessionID, handleID, offset, buff) } } fun writeFile(handleID: Int, offset: Long, buff: ByteArray, buff_size: Int): Int { - synchronized(this){ + synchronized(applicationContext) { return native_write_file(sessionID, handleID, offset, buff, buff_size) } } @@ -237,4 +237,40 @@ class GocryptfsVolume(var sessionID: Int) { null } } + + fun loadWholeFile(fullPath: String, maxSize: Long? = null): Pair { + val fileSize = getSize(fullPath) + return if (fileSize >= 0) { + maxSize?.let { + if (fileSize > it) { + return Pair(null, 0) + } + } + try { + val fileBuff = ByteArray(fileSize.toInt()) + val handleID = openReadMode(fullPath) + if (handleID == -1) { + Pair(null, 3) + } else { + var offset: Long = 0 + val ioBuffer = ByteArray(DefaultBS) + var length: Int + while (readFile(handleID, offset, ioBuffer).also { length = it } > 0) { + System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length) + offset += length.toLong() + } + closeFile(handleID) + if (offset == fileBuff.size.toLong()) { + Pair(fileBuff, 0) + } else { + Pair(null, 4) + } + } + } catch (e: OutOfMemoryError) { + Pair(null, 2) + } + } else { + Pair(null, 1) + } + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt index c205441..889deda 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt @@ -170,7 +170,7 @@ class OpenActivity : VolumeActionActivity() { if (success){ startExplorer() } else { - GocryptfsVolume(sessionID).close() + GocryptfsVolume(applicationContext, sessionID).close() } } } @@ -257,7 +257,7 @@ class OpenActivity : VolumeActionActivity() { if (intent.action == "pick" && !isFinishingIntentionally){ val sessionID = intent.getIntExtra("sessionID", -1) if (sessionID != -1){ - GocryptfsVolume(sessionID).close() + GocryptfsVolume(applicationContext, sessionID).close() RestrictedFileProvider.wipeAll(this) } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt index c5a9ceb..aa150ea 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt @@ -1,14 +1,18 @@ package sushi.hardcore.droidfs.adapters -import android.content.Context -import android.view.LayoutInflater +import android.graphics.drawable.Drawable import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView -import sushi.hardcore.droidfs.ConstValues.Companion.getAssociatedDrawable +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.DrawableImageViewTarget +import com.bumptech.glide.request.transition.Transition +import sushi.hardcore.droidfs.ConstValues +import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.util.PathUtils @@ -16,17 +20,17 @@ import java.text.DateFormat import java.util.* class ExplorerElementAdapter( - context: Context, - private val onExplorerElementClick: (Int) -> Unit, - private val onExplorerElementLongClick: (Int) -> Unit + val activity: AppCompatActivity, + val gocryptfsVolume: GocryptfsVolume?, + val onExplorerElementClick: (Int) -> Unit, + val onExplorerElementLongClick: (Int) -> Unit ) : RecyclerView.Adapter() { - private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) + val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) var explorerElements = listOf() set(value) { field = value unSelectAll() } - private val inflater: LayoutInflater = LayoutInflater.from(context) val selectedItems: MutableList = ArrayList() override fun getItemCount(): Int { @@ -72,11 +76,7 @@ class ExplorerElementAdapter( notifyDataSetChanged() } - open class ExplorerElementViewHolder( - itemView: View, - private val onClick: (Int) -> Boolean, - private val onLongClick: (Int) -> Boolean, - ) : RecyclerView.ViewHolder(itemView) { + open class ExplorerElementViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val textElementName by lazy { itemView.findViewById(R.id.text_element_name) } @@ -99,57 +99,87 @@ class ExplorerElementAdapter( open fun bind(explorerElement: ExplorerElement, position: Int) { textElementName.text = explorerElement.name - selectableContainer.setOnClickListener { - setBackground(onClick(position)) - } - selectableContainer.setOnLongClickListener { - setBackground(onLongClick(position)) - true + (bindingAdapter as ExplorerElementAdapter?)?.let { adapter -> + selectableContainer.setOnClickListener { + setBackground(adapter.onItemClick(position)) + } + selectableContainer.setOnLongClickListener { + setBackground(adapter.onItemLongClick(position)) + true + } } } } - open class RegularElementViewHolder( - itemView: View, - private val dateFormat: DateFormat, - onClick: (Int) -> Boolean, - onLongClick: (Int) -> Boolean, - ) : ExplorerElementViewHolder(itemView, onClick, onLongClick) { + open class RegularElementViewHolder(itemView: View) : ExplorerElementViewHolder(itemView) { open fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { super.bind(explorerElement, position) textElementSize.text = PathUtils.formatSize(explorerElement.size) - textElementMtime.text = dateFormat.format(explorerElement.mTime) + (bindingAdapter as ExplorerElementAdapter?)?.let { + textElementMtime.text = it.dateFormat.format(explorerElement.mTime) + } setBackground(isSelected) } } - class FileViewHolder( - itemView: View, - dateFormat: DateFormat, - onClick: (Int) -> Boolean, - onLongClick: (Int) -> Boolean, - ) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) { + class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) { + var displayThumbnail = true + var target: DrawableImageViewTarget? = null + + private fun loadThumbnail(fullPath: String) { + (bindingAdapter as ExplorerElementAdapter?)?.let { adapter -> + adapter.gocryptfsVolume?.let { volume -> + displayThumbnail = true + Thread { + volume.loadWholeFile(fullPath, 50_000_000).first?.let { + if (displayThumbnail) { + adapter.activity.runOnUiThread { + if (displayThumbnail) { + target = Glide.with(adapter.activity).load(it).into(object : DrawableImageViewTarget(icon) { + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + super.onResourceReady(resource, transition) + target = null + } + }) + } + } + } + } + }.start() + } + } + } override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { super.bind(explorerElement, position, isSelected) - icon.setImageResource(getAssociatedDrawable(explorerElement.name)) + icon.setImageResource( + when { + ConstValues.isImage(explorerElement.name) -> { + loadThumbnail(explorerElement.fullPath) + R.drawable.icon_file_image + } + ConstValues.isVideo(explorerElement.name) -> { + loadThumbnail(explorerElement.fullPath) + R.drawable.icon_file_video + } + ConstValues.isText(explorerElement.name) -> R.drawable.icon_file_text + ConstValues.isAudio(explorerElement.name) -> R.drawable.icon_file_audio + else -> R.drawable.icon_file_unknown + } + ) } } - class DirectoryViewHolder( - itemView: View, - dateFormat: DateFormat, - onClick: (Int) -> Boolean, - onLongClick: (Int) -> Boolean, - ) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) { + + class DirectoryViewHolder(itemView: View) : RegularElementViewHolder(itemView) { override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { super.bind(explorerElement, position, isSelected) icon.setImageResource(R.drawable.icon_folder) } } - class ParentFolderViewHolder( - itemView: View, - onClick: (Int) -> Boolean, - onLongClick: (Int) -> Boolean, - ): ExplorerElementViewHolder(itemView, onClick, onLongClick) { + + class ParentFolderViewHolder(itemView: View): ExplorerElementViewHolder(itemView) { override fun bind(explorerElement: ExplorerElement, position: Int) { super.bind(explorerElement, position) textElementSize.text = "" @@ -158,12 +188,22 @@ class ExplorerElementAdapter( } } + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if (holder is FileViewHolder) { + //cancel pending thumbnail display + holder.displayThumbnail = false + holder.target?.let { + Glide.with(activity).clear(it) + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = inflater.inflate(R.layout.adapter_explorer_element, parent, false) + val view = activity.layoutInflater.inflate(R.layout.adapter_explorer_element, parent, false) return when (viewType) { - ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick) - ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick) - ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view, ::onItemClick, ::onItemLongClick) + ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view) + ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view) + ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view) else -> throw IllegalArgumentException() } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index ab0a05e..5ba39f7 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -78,7 +78,7 @@ open class BaseExplorerActivity : BaseActivity() { usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) volumeName = intent.getStringExtra("volume_name") ?: "" val sessionID = intent.getIntExtra("sessionID", -1) - gocryptfsVolume = GocryptfsVolume(sessionID) + gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID) sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries) sortOrderValues = resources.getStringArray(R.array.sort_orders_values) foldersFirst = sharedPrefs.getBoolean("folders_first", true) @@ -95,7 +95,16 @@ open class BaseExplorerActivity : BaseActivity() { setSupportActionBar(toolbar) title = "" titleText.text = getString(R.string.volume, volumeName) - explorerAdapter = ExplorerElementAdapter(this, ::onExplorerItemClick, ::onExplorerItemLongClick) + explorerAdapter = ExplorerElementAdapter( + this, + if (sharedPrefs.getBoolean("thumbnails", true)) { + gocryptfsVolume + } else { + null + }, + ::onExplorerItemClick, + ::onExplorerItemLongClick + ) explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java) currentDirectoryPath = explorerViewModel.currentDirectoryPath setCurrentPath(currentDirectoryPath) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index 1698f04..e03dfd9 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -36,7 +36,7 @@ class ExplorerActivity : BaseExplorerActivity() { if (result.resultCode == Activity.RESULT_OK) { result.data?.let { resultIntent -> val remoteSessionID = resultIntent.getIntExtra("sessionID", -1) - val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID) + val remoteGocryptfsVolume = GocryptfsVolume(applicationContext, remoteSessionID) val path = resultIntent.getStringExtra("path") val operationFiles = ArrayList() if (path == null){ //multiples elements diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt index 2c89d1d..660b305 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt @@ -86,8 +86,7 @@ class ExplorerActivityPick : BaseExplorerActivity() { if (!isFinishingIntentionally && !usf_keep_open){ val sessionID = intent.getIntExtra("originalSessionID", -1) if (sessionID != -1){ - val v = GocryptfsVolume(sessionID) - v.close() + GocryptfsVolume(applicationContext, sessionID).close() } super.closeVolumeOnDestroy() } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt index 7b170d6..fbf05a8 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt @@ -30,7 +30,7 @@ abstract class FileViewerActivity: BaseActivity() { filePath = intent.getStringExtra("path")!! originalParentPath = PathUtils.getParentPath(filePath) val sessionID = intent.getIntExtra("sessionID", -1) - gocryptfsVolume = GocryptfsVolume(sessionID) + gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) foldersFirst = sharedPrefs.getBoolean("folders_first", true) windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) @@ -56,51 +56,20 @@ abstract class FileViewerActivity: BaseActivity() { } protected fun loadWholeFile(path: String): ByteArray? { - val fileSize = gocryptfsVolume.getSize(path) - if (fileSize >= 0){ - try { - val fileBuff = ByteArray(fileSize.toInt()) - var success = false - val handleID = gocryptfsVolume.openReadMode(path) - if (handleID != -1) { - var offset: Long = 0 - val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS) - var length: Int - while (gocryptfsVolume.readFile(handleID, offset, ioBuffer).also { length = it } > 0){ - System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length) - offset += length.toLong() - } - gocryptfsVolume.closeFile(handleID) - success = offset == fileBuff.size.toLong() - } - if (success){ - return fileBuff - } else { - CustomAlertDialogBuilder(this, themeValue) - .setTitle(R.string.error) - .setMessage(R.string.read_file_failed) - .setCancelable(false) - .setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() } - .show() - } - } catch (e: OutOfMemoryError){ - CustomAlertDialogBuilder(this, themeValue) - .setTitle(R.string.error) - .setMessage(R.string.outofmemoryerror_msg) - .setCancelable(false) - .setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() } - .show() - } - - } else { - CustomAlertDialogBuilder(this, themeValue) + val result = gocryptfsVolume.loadWholeFile(path) + if (result.second != 0) { + val dialog = CustomAlertDialogBuilder(this, themeValue) .setTitle(R.string.error) - .setMessage(R.string.get_size_failed) .setCancelable(false) .setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() } - .show() + when (result.second) { + 1 -> dialog.setMessage(R.string.get_size_failed) + 2 -> dialog.setMessage(R.string.outofmemoryerror_msg) + else -> dialog.setMessage(R.string.read_file_failed) + } + dialog.show() } - return null + return result.first } protected fun createPlaylist() { diff --git a/app/src/main/res/drawable/icon_image.xml b/app/src/main/res/drawable/icon_image.xml new file mode 100644 index 0000000..70560b9 --- /dev/null +++ b/app/src/main/res/drawable/icon_image.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67a6acf..ac4debd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,4 +204,6 @@ Encryption cipher: Theme Customize app theme + Thumbnails + Show images and videos thumbnails diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 49139bd..05f9c9a 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -38,6 +38,13 @@ android:title="@string/folders_first" android:summary="@string/folders_first_summary"/> + +