diff --git a/BUILD.md b/BUILD.md index ddf9961..3a28ed1 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,18 +1,21 @@ # Introduction DroidFS relies on modified versions of the original encrypted filesystems programs to open volumes. [CryFS](https://github.com/cryfs/cryfs) is written in C++ while [gocryptfs](https://github.com/rfjakob/gocryptfs) is written in [Go](https://golang.org). Thus, building DroidFS requires the compilation of native code. However, for the sake of simplicity, the application has been designed in a modular way: you can build a version of DroidFS that supports both Gocryptfs and CryFS, or only one of the two. -Moreover, DroidFS aims to be accessible to as many people as possible. If you encounter any problems or need help with the build, feel free to open an issue, a discussion, or contact me by [email](mailto:hardcore.sushi@disroot.org) or on [Matrix](https://matrix.org): @hardcoresushi:matrix.underworld.fr +Moreover, DroidFS aims to be accessible to as many people as possible. If you encounter any problems or need help with the build, feel free to open an issue, a discussion, or contact me (currently the main developer) by [email](mailto:gh@arkensys.dedyn.io) or on [Matrix](https://matrix.org): @hardcoresushi:matrix.underworld.fr # Setup + +The following two steps assume you're using a Debian-based Linux distribution. Package names might be similar for other distributions. Don't hesitate to ask if you're having trouble with this. + Install required packages: ``` -$ sudo apt-get install openjdk-11-jdk-headless build-essential pkg-config git gnupg2 wget apksigner +$ sudo apt-get install openjdk-17-jdk-headless build-essential pkg-config git gnupg2 wget apksigner npm ``` -You also need to manually install the [Android SDK](https://developer.android.com/studio/index.html#command-tools) and the [Android Native Development Kit (NDK)](https://developer.android.com/ndk/downloads) (r23 versions are recommended). +You also need to manually install the [Android SDK](https://developer.android.com/studio/index.html#command-tools) and the [Android Native Development Kit (NDK)](https://github.com/android/ndk/wiki/Unsupported-Downloads#r25c) version `25.2.9519653` (r25c). libcryfs cannot be built with newer NDK versions at the moment due to compatibility issues with [boost](https://www.boost.org). If you succeed in building it with a more recent version of NDK, please report it. -If you want a support for Gocryptfs volumes, you must install [Go](https://golang.org/doc/install) and libssl: +If you want a support for Gocryptfs volumes, you need to install [Go](https://golang.org/doc/install): ``` -$ sudo apt-get install golang-go libssl-dev +$ sudo apt-get install golang-go ``` The code should be authenticated before being built. To verify the signatures, you will need my PGP key: ``` @@ -45,16 +48,16 @@ $ git clone --depth=1 https://git.ffmpeg.org/ffmpeg.git If you want Gocryptfs support, you need to download OpenSSL: ``` $ cd ../libgocryptfs -$ wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz +$ wget https://openssl.org/source/openssl-3.3.1.tar.gz ``` Verify OpenSSL signature: ``` -$ wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz.asc -$ gpg --verify openssl-1.1.1w.tar.gz.asc openssl-1.1.1w.tar.gz +$ https://openssl.org/source/openssl-3.3.1.tar.gz.asc +$ gpg --verify openssl-3.3.1.tar.gz.asc openssl-3.3.1.tar.gz ``` Continue **ONLY** if the signature is **VALID**. ``` -$ tar -xzf openssl-1.1.1w.tar.gz +$ tar -xzf openssl-3.3.1.tar.gz ``` If you want CryFS support, initialize libcryfs: ``` @@ -62,14 +65,6 @@ $ cd app/libcryfs $ git submodule update --depth=1 --init ``` -To be able to open PDF files internally, [pdf.js](https://github.com/mozilla/pdf.js) must be downloaded: -``` -$ mkdir libpdfviewer/app/pdfjs-dist && cd libpdfviewer/app/pdfjs-dist -$ wget https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.8.162.tgz -$ tar xf pdfjs-dist-3.8.162.tgz package/build/pdf.min.js package/build/pdf.worker.min.js -$ mv package/build . && rm pdfjs-dist-3.8.162.tgz -``` - # Build Retrieve your Android NDK installation path, usually something like `/home/\/Android/SDK/ndk/\`. Then, make it available in your shell: ``` @@ -84,8 +79,8 @@ $ ./build.sh ffmpeg This step is only required if you want Gocryptfs support. ``` $ cd app/libgocryptfs -$ OPENSSL_PATH="./openssl-1.1.1w" ./build.sh - ``` +$ ANDROID_NDK_ROOT="$ANDROID_NDK_HOME" OPENSSL_PATH="./openssl-3.3.1" ./build.sh +``` ## Compile APKs Gradle build libgocryptfs and libcryfs by default. diff --git a/README.md b/README.md index 38d21a4..37aab3c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Some available features are considered risky and are therefore disabled by defau -\* These features may require temporarily writing the plain file to disk (DroidFS internal storage). This file can be read by applications with root access or by physical access if your device is not encrypted. For files small enough and on a 3.17+ kernel, DroidFS will try to use memory-only storage using `memfd_create(2)` (can break some apps). +\* These features can work in two ways: temporarily writing the plain file to disk (DroidFS internal storage) or sharing it via memory. By default, DroidFS will choose to keep the file only in memory as it's more secure, but will fallback to disk export if the file is too large to be held in memory. This behavior can be changed with the *"Export method"* parameter in the settings. Please note that some applications require the file to be stored on disk, and therefore do not work with memory-exported files. # Download diff --git a/app/build.gradle b/app/build.gradle index 2ac7e3d..557056c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ if (hasProperty("nosplits")) { android { compileSdk 34 - ndkVersion "25.2.9519653" + ndkVersion '25.2.9519653' namespace "sushi.hardcore.droidfs" compileOptions { @@ -58,6 +58,7 @@ android { splits { abi { enable true + reset() // fix unknown bug (https://ru.stackoverflow.com/questions/1557805/abis-armeabi-mips-mips64-riscv64-are-not-supported-for-platform) universalApk true } } @@ -103,28 +104,28 @@ android { dependencies { implementation project(":libpdfviewer:app") - implementation 'androidx.core:core-ktx:1.12.0' - implementation "androidx.appcompat:appcompat:1.6.1" + implementation 'androidx.core:core-ktx:1.13.1' + implementation "androidx.appcompat:appcompat:1.7.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" - def lifecycle_version = "2.6.2" + def lifecycle_version = "2.8.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" - implementation "androidx.sqlite:sqlite-ktx:2.3.1" + implementation "androidx.sqlite:sqlite-ktx:2.4.0" implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'com.github.bumptech.glide:glide:4.16.0' implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" - def media3_version = "1.1.1" + def media3_version = "1.3.1" implementation "androidx.media3:media3-exoplayer:$media3_version" - implementation 'androidx.media3:media3-ui:1.1.1' + implementation "androidx.media3:media3-ui:$media3_version" implementation "androidx.media3:media3-datasource:$media3_version" implementation "androidx.concurrent:concurrent-futures:1.1.0" - def camerax_version = "1.3.0-rc02" + def camerax_version = "1.3.3" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:$camerax_version" diff --git a/app/ffmpeg/build.sh b/app/ffmpeg/build.sh index c798e6a..e63ea62 100755 --- a/app/ffmpeg/build.sh +++ b/app/ffmpeg/build.sh @@ -74,7 +74,7 @@ else --disable-appkit \ --disable-alsa \ --disable-debug \ - >/dev/null && + && make -j $(nproc --all) >/dev/null) && mkdir -p build/$1/libavformat build/$1/libavcodec build/$1/libavutil && cp $FFMPEG_DIR/libavformat/*.h $FFMPEG_DIR/libavformat/libavformat.so build/$1/libavformat && diff --git a/app/libcryfs b/app/libcryfs index 6388eaf..0398d48 160000 --- a/app/libcryfs +++ b/app/libcryfs @@ -1 +1 @@ -Subproject commit 6388eaf433a4196f10389921d5e346c90ee3d793 +Subproject commit 0398d48b0963c01092976c5c7012b02327e564f0 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46b3699..f045d33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,6 +53,7 @@ + diff --git a/app/src/main/java/androidx/camera/video/originals/README.md b/app/src/main/java/androidx/camera/video/originals/README.md index 9e619dd..d7b0df5 100644 --- a/app/src/main/java/androidx/camera/video/originals/README.md +++ b/app/src/main/java/androidx/camera/video/originals/README.md @@ -5,7 +5,7 @@ Create the `new` folder if needed: mkdir -p new ``` -Put new CameraX files from upstream in the `new` folder. +Put the new CameraX files from upstream (`androidx.camera.video.Recorder`, `androidx.camera.video.Recording`, `androidx.camera.video.PendingRecording` and `androidx.camera.video.internal.encoder.EncoderImpl`) in the `new` folder. Perform the 3 way merge: ``` diff --git a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt index 5d9ec04..4b43864 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt @@ -16,7 +16,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.ObjRef -import sushi.hardcore.droidfs.util.WidgetUtil +import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.util.* @@ -89,8 +89,8 @@ class ChangePasswordActivity: BaseActivity() { } private fun changeVolumePassword() { - val newPassword = WidgetUtil.encodeEditTextContent(binding.editNewPassword) - val newPasswordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm) + val newPassword = UIUtils.encodeEditTextContent(binding.editNewPassword) + val newPasswordConfirm = UIUtils.encodeEditTextContent(binding.editPasswordConfirm) @SuppressLint("NewApi") if (!newPassword.contentEquals(newPasswordConfirm)) { Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() @@ -135,7 +135,7 @@ class ChangePasswordActivity: BaseActivity() { null } val currentPassword = if (givenHash == null) { - WidgetUtil.encodeEditTextContent(binding.editCurrentPassword) + UIUtils.encodeEditTextContent(binding.editCurrentPassword) } else { null } diff --git a/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt index 5a667b6..f4837fa 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt @@ -7,6 +7,7 @@ import android.os.Handler import android.os.ParcelFileDescriptor import android.system.Os import android.util.Log +import androidx.preference.PreferenceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import sushi.hardcore.droidfs.filesystems.EncryptedVolume @@ -22,6 +23,23 @@ class EncryptedFileProvider(context: Context) { companion object { private const val TAG = "EncryptedFileProvider" fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp") + + var exportMethod = ExportMethod.AUTO + } + + enum class ExportMethod { + AUTO, + DISK, + MEMORY; + + companion object { + fun parse(value: String) = when (value) { + "auto" -> EncryptedFileProvider.ExportMethod.AUTO + "disk" -> EncryptedFileProvider.ExportMethod.DISK + "memory" -> EncryptedFileProvider.ExportMethod.MEMORY + else -> throw IllegalArgumentException("Invalid export method: $value") + } + } } private val memoryInfo = ActivityManager.MemoryInfo() @@ -33,6 +51,11 @@ class EncryptedFileProvider(context: Context) { (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo( memoryInfo ) + + PreferenceManager.getDefaultSharedPreferences(context) + .getString("export_method", null)?.let { + exportMethod = ExportMethod.parse(it) + } } class ExportedDiskFile private constructor( @@ -118,16 +141,18 @@ class EncryptedFileProvider(context: Context) { path: String, size: Long, ): ExportedFile? { - return if (size > memoryInfo.availMem * 0.8) { - ExportedDiskFile.create( - path, - tmpFilesDir, - handler, - ) - } else if (isMemFileSupported) { - ExportedMemFile.create(path, size) as ExportedFile - } else { - null + val diskFile by lazy { ExportedDiskFile.create(path, tmpFilesDir, handler) } + val memFile by lazy { ExportedMemFile.create(path, size) } + return when (exportMethod) { + ExportMethod.MEMORY -> memFile + ExportMethod.DISK -> diskFile + ExportMethod.AUTO -> { + if (isMemFileSupported && size < memoryInfo.availMem * 0.8) { + memFile + } else { + diskFile + } + } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/FileTypes.kt b/app/src/main/java/sushi/hardcore/droidfs/FileTypes.kt index 986574b..2020c24 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/FileTypes.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/FileTypes.kt @@ -6,7 +6,7 @@ object FileTypes { private val FILE_EXTENSIONS = mapOf( Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp", "heic")), Pair("video", listOf("mp4", "webm", "mkv", "mov")), - Pair("audio", listOf("mp3", "ogg", "m4a", "wav", "flac")), + Pair("audio", listOf("mp3", "ogg", "m4a", "wav", "flac", "opus")), Pair("pdf", listOf("pdf")), Pair("text", listOf( "asc", diff --git a/app/src/main/java/sushi/hardcore/droidfs/LogcatActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/LogcatActivity.kt new file mode 100644 index 0000000..3eac7e2 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/LogcatActivity.kt @@ -0,0 +1,88 @@ +package sushi.hardcore.droidfs + +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import sushi.hardcore.droidfs.databinding.ActivityLogcatBinding +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.io.OutputStreamWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class LogcatActivity: BaseActivity() { + private lateinit var binding: ActivityLogcatBinding + private var process: Process? = null + private val dateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.getDefault()) + } + private val saveAs = registerForActivityResult(ActivityResultContracts.CreateDocument("text/*")) { uri -> + uri?.let { + saveTo(it) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLogcatBinding.inflate(layoutInflater) + setContentView(binding.root) + title = getString(R.string.logcat_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + lifecycleScope.launch(Dispatchers.IO) { + try { + BufferedReader(InputStreamReader(Runtime.getRuntime().exec("logcat").also { + process = it + }.inputStream)).forEachLine { + binding.content.post { + binding.content.append("$it\n") + } + } + } catch (_: InterruptedIOException) {} + } + } + + override fun onDestroy() { + super.onDestroy() + process?.destroy() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.logcat, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + R.id.save -> { + saveAs.launch("DroidFS_${dateFormat.format(Date())}.log") + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun saveTo(uri: Uri) { + lifecycleScope.launch(Dispatchers.IO) { + BufferedWriter(OutputStreamWriter(contentResolver.openOutputStream(uri))).use { + it.write(binding.content.text.toString()) + } + launch(Dispatchers.Main) { + Toast.makeText(this@LogcatActivity, R.string.logcat_saved, Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt index 67faa69..ec681b7 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt @@ -28,6 +28,7 @@ import sushi.hardcore.droidfs.file_operations.FileOperationService import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.PathUtils +import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.EditTextDialog import java.io.File @@ -354,7 +355,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main_activity, menu) - menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode + val settingsVisible = !explorerRouter.pickMode && !explorerRouter.dropMode + menu.findItem(R.id.settings).isVisible = settingsVisible + if (settingsVisible) { + UIUtils.getMenuIconNeutralTint(this, menu).applyTo(R.id.settings, R.drawable.icon_settings) + } val isSelecting = volumeAdapter.selectedItems.isNotEmpty() menu.findItem(R.id.select_all).isVisible = isSelecting menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any { diff --git a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt index f455eff..35cfa6b 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt @@ -1,5 +1,6 @@ package sushi.hardcore.droidfs +import android.app.ActivityOptions import android.content.Intent import android.content.SharedPreferences import android.os.Build @@ -90,9 +91,15 @@ class SettingsActivity : BaseActivity() { private fun refreshTheme() { with(requireActivity()) { - startActivity(Intent(this, SettingsActivity::class.java)) + startActivity( + Intent(this, SettingsActivity::class.java), + ActivityOptions.makeCustomAnimation( + this, + android.R.anim.fade_in, + android.R.anim.fade_out + ).toBundle() + ) finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) } } @@ -120,6 +127,10 @@ class SettingsActivity : BaseActivity() { false } } + findPreference("logcat")?.setOnPreferenceClickListener { _ -> + startActivity(Intent(requireContext(), LogcatActivity::class.java)) + true + } } } @@ -179,17 +190,7 @@ class SettingsActivity : BaseActivity() { true } switchExpose.setOnPreferenceChangeListener { _, checked -> - if (checked as Boolean) { - if (!Compat.isMemFileSupported()) { - CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme) - .setTitle(R.string.error) - .setMessage("Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of ${Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION}.") - .setPositiveButton(R.string.ok, null) - .show() - return@setOnPreferenceChangeListener false - } - } - VolumeProvider.usfExpose = checked + VolumeProvider.usfExpose = checked as Boolean updateView(usfExpose = checked) VolumeProvider.notifyRootsChanged(requireContext()) true @@ -199,6 +200,19 @@ class SettingsActivity : BaseActivity() { TemporaryFileProvider.usfSafWrite = checked true } + + findPreference("export_method")!!.setOnPreferenceChangeListener { _, newValue -> + if (newValue as String == "memory" && !Compat.isMemFileSupported()) { + CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme) + .setTitle(R.string.error) + .setMessage(getString(R.string.memfd_create_unsupported, Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION)) + .setPositiveButton(R.string.ok, null) + .show() + return@setOnPreferenceChangeListener false + } + EncryptedFileProvider.exportMethod = EncryptedFileProvider.ExportMethod.parse(newValue) + true + } } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt index cfbf18d..6dba779 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt @@ -13,7 +13,7 @@ import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.util.ObjRef -import sushi.hardcore.droidfs.util.WidgetUtil +import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.util.* @@ -123,7 +123,7 @@ class VolumeOpener( apply() } } - val password = WidgetUtil.encodeEditTextContent(dialogBinding!!.editPassword) + val password = UIUtils.encodeEditTextContent(dialogBinding!!.editPassword) val savePasswordHash = dialogBinding!!.checkboxSavePassword.isChecked dialogBinding = null // openVolumeWithPassword is responsible for wiping the password diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt index c472a91..a0c796c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt @@ -20,7 +20,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.util.Compat import sushi.hardcore.droidfs.util.ObjRef -import sushi.hardcore.droidfs.util.WidgetUtil +import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import java.io.File import java.util.* @@ -146,8 +146,8 @@ class CreateVolumeFragment: Fragment() { } private fun createVolume() { - val password = WidgetUtil.encodeEditTextContent(binding.editPassword) - val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm) + val password = UIUtils.encodeEditTextContent(binding.editPassword) + val passwordConfirm = UIUtils.encodeEditTextContent(binding.editPasswordConfirm) if (!password.contentEquals(passwordConfirm)) { Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() Arrays.fill(password, 0) diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt index 9752050..60a2d35 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt @@ -236,14 +236,21 @@ class VolumeProvider: DocumentsProvider() { ): String? { if (!usfExpose || !usfSafWrite) return null val document = parseDocumentId(parentDocumentId) ?: return null - val newFile = PathUtils.pathJoin(document.path, displayName) - val f = document.encryptedVolume.openFileWriteMode(newFile) - return if (f == -1L) { - Log.e(TAG, "Failed to create file: $document") - null + val path = PathUtils.pathJoin(document.path, displayName) + var success = false + if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { + success = document.encryptedVolume.mkdir(path) } else { - document.encryptedVolume.closeFile(f) - document.rootId+newFile + val f = document.encryptedVolume.openFileWriteMode(path) + if (f != -1L) { + document.encryptedVolume.closeFile(f) + success = true + } + } + return if (success) { + document.rootId+path + } else { + null } } 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 f07c792..1955915 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -11,10 +11,12 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageButton +import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast import androidx.activity.addCallback import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -23,10 +25,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.EncryptedFileProvider @@ -49,6 +54,7 @@ import sushi.hardcore.droidfs.file_viewers.VideoPlayer import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.util.PathUtils +import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.EditTextDialog @@ -69,6 +75,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } protected lateinit var fileOperationService: FileOperationService protected val activityScope = MainScope() + private var directoryLoadingTask: Job? = null protected lateinit var explorerElements: MutableList protected lateinit var explorerAdapter: ExplorerElementAdapter protected lateinit var app: VolumeManagerApp @@ -79,6 +86,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene private lateinit var titleText: TextView private lateinit var recycler_view_explorer: RecyclerView private lateinit var refresher: SwipeRefreshLayout + private lateinit var loader: ProgressBar private lateinit var textDirEmpty: TextView private lateinit var currentPathText: TextView private lateinit var numberOfFilesText: TextView @@ -101,6 +109,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene init() recycler_view_explorer = findViewById(R.id.recycler_view_explorer) refresher = findViewById(R.id.refresher) + loader = findViewById(R.id.loader) textDirEmpty = findViewById(R.id.text_dir_empty) currentPathText = findViewById(R.id.current_path_text) numberOfFilesText = findViewById(R.id.number_of_files_text) @@ -259,6 +268,27 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene .show() } + protected fun createNewFile(callback: (Long) -> Unit) { + EditTextDialog(this, R.string.enter_file_name) { + if (it.isEmpty()) { + Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() + createNewFile(callback) + } else { + val filePath = PathUtils.pathJoin(currentDirectoryPath, it) + val handleID = encryptedVolume.openFileWriteMode(filePath) + if (handleID == -1L) { + CustomAlertDialogBuilder(this, theme) + .setTitle(R.string.error) + .setMessage(R.string.file_creation_failed) + .setPositiveButton(R.string.ok, null) + .show() + } else { + callback(handleID) + } + } + }.show() + } + private fun setVolumeNameTitle() { titleText.text = getString(R.string.volume, volumeName) } @@ -312,17 +342,15 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } private fun displayExplorerElements() { - synchronized(this) { - ExplorerElement.sortBy(sortOrderValues[currentSortOrderIndex], foldersFirst, explorerElements) - } + ExplorerElement.sortBy(sortOrderValues[currentSortOrderIndex], foldersFirst, explorerElements) unselectAll(false) + loader.isVisible = false + recycler_view_explorer.isVisible = true explorerAdapter.explorerElements = explorerElements - val sharedPrefsEditor = sharedPrefs.edit() - sharedPrefsEditor.putString(Constants.SORT_ORDER_KEY, sortOrderValues[currentSortOrderIndex]) - sharedPrefsEditor.apply() } - private fun recursiveSetSize(directory: ExplorerElement) { + private suspend fun recursiveSetSize(directory: ExplorerElement) { + yield() for (child in encryptedVolume.readDir(directory.fullPath) ?: return) { if (child.isDirectory) { recursiveSetSize(child) @@ -346,15 +374,16 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } } - protected fun setCurrentPath(path: String, onDisplayed: (() -> Unit)? = null) { - synchronized(this) { - explorerElements = encryptedVolume.readDir(path) ?: return - if (path != "/") { - explorerElements.add( - 0, - ExplorerElement("..", Stat.parentFolderStat(), parentPath = currentDirectoryPath) - ) - } + protected fun setCurrentPath(path: String, onDisplayed: (() -> Unit)? = null) = lifecycleScope.launch { + directoryLoadingTask?.cancelAndJoin() + recycler_view_explorer.isVisible = false + loader.isVisible = true + explorerElements = encryptedVolume.readDir(path) ?: return@launch + if (path != "/") { + explorerElements.add( + 0, + ExplorerElement("..", Stat.parentFolderStat(), parentPath = currentDirectoryPath) + ) } textDirEmpty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.GONE currentDirectoryPath = path @@ -362,22 +391,19 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile }) displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory }) if (mapFolders) { - lifecycleScope.launch { - var totalSize: Long = 0 - withContext(Dispatchers.IO) { - synchronized(this@BaseExplorerActivity) { - for (element in explorerElements) { - if (element.isDirectory) { - recursiveSetSize(element) - } - totalSize += element.stat.size - } + var totalSize: Long = 0 + directoryLoadingTask = launch(Dispatchers.IO) { + for (element in explorerElements) { + if (element.isDirectory) { + recursiveSetSize(element) } + totalSize += element.stat.size } - displayExplorerElements() - totalSizeText.text = getString(R.string.total_size, PathUtils.formatSize(totalSize)) - onDisplayed?.invoke() } + directoryLoadingTask!!.join() + displayExplorerElements() + totalSizeText.text = getString(R.string.total_size, PathUtils.formatSize(totalSize)) + onDisplayed?.invoke() } else { displayExplorerElements() totalSizeText.text = getString( @@ -560,14 +586,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } } - private fun setMenuIconTint(menu: Menu, iconColor: Int, menuItemId: Int, drawableId: Int) { - menu.findItem(menuItemId)?.let { - it.icon = ContextCompat.getDrawable(this, drawableId)?.apply { - setTint(iconColor) - } - } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menu.findItem(R.id.rename).isVisible = false menu.findItem(R.id.open_as)?.isVisible = false @@ -575,9 +593,10 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene menu.findItem(R.id.external_open)?.isVisible = false } val noItemSelected = explorerAdapter.selectedItems.isEmpty() - val iconColor = ContextCompat.getColor(this, R.color.neutralIconTint) - setMenuIconTint(menu, iconColor, R.id.sort, R.drawable.icon_sort) - setMenuIconTint(menu, iconColor, R.id.share, R.drawable.icon_share) + with(UIUtils.getMenuIconNeutralTint(this, menu)) { + applyTo(R.id.sort, R.drawable.icon_sort) + applyTo(R.id.share, R.drawable.icon_share) + } menu.findItem(R.id.sort).isVisible = noItemSelected menu.findItem(R.id.lock).isVisible = noItemSelected menu.findItem(R.id.close).isVisible = noItemSelected @@ -607,7 +626,13 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene .setTitle(R.string.sort_order) .setSingleChoiceItems(sortOrderEntries, currentSortOrderIndex) { dialog, which -> currentSortOrderIndex = which - displayExplorerElements() + // displayExplorerElements must not be called if directoryLoadingTask is active + if (directoryLoadingTask?.isActive != true) { + displayExplorerElements() + } + val sharedPrefsEditor = sharedPrefs.edit() + sharedPrefsEditor.putString(Constants.SORT_ORDER_KEY, sortOrderValues[currentSortOrderIndex]) + sharedPrefsEditor.apply() dialog.dismiss() } .setNegativeButton(R.string.cancel, null) 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 6f0e619..a552c52 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -68,7 +68,11 @@ class ExplorerActivity : BaseExplorerActivity() { private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> if (uris != null) { for (uri in uris) { - contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + try { + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } catch (e: SecurityException) { + e.printStackTrace() + } } importFilesFromUris(uris) { onImportComplete(uris) @@ -189,9 +193,11 @@ class ExplorerActivity : BaseExplorerActivity() { pickImportDirectory.launch(null) } "createFile" -> { - EditTextDialog(this, R.string.enter_file_name) { - createNewFile(it) - }.show() + createNewFile { + encryptedVolume.closeFile(it) + setCurrentPath(currentDirectoryPath) + invalidateOptionsMenu() + } } "createFolder" -> { openDialogCreateFolder() @@ -219,26 +225,6 @@ class ExplorerActivity : BaseExplorerActivity() { cancelItemAction() } - private fun createNewFile(fileName: String){ - if (fileName.isEmpty()) { - Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() - } else { - val filePath = PathUtils.pathJoin(currentDirectoryPath, fileName) - val handleID = encryptedVolume.openFileWriteMode(filePath) - if (handleID == -1L) { - CustomAlertDialogBuilder(this, theme) - .setTitle(R.string.error) - .setMessage(R.string.file_creation_failed) - .setPositiveButton(R.string.ok, null) - .show() - } else { - encryptedVolume.closeFile(handleID) - setCurrentPath(currentDirectoryPath) - invalidateOptionsMenu() - } - } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.explorer, menu) val result = super.onCreateOptionsMenu(menu) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt index c2d9080..5c5f4cc 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt @@ -9,6 +9,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets class ExplorerActivityDrop : BaseExplorerActivity() { @@ -30,15 +32,15 @@ class ExplorerActivityDrop : BaseExplorerActivity() { return when (item.itemId) { R.id.validate -> { val extras = intent.extras - val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) { + val success = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) { when (intent.action) { Intent.ACTION_SEND -> { val uri = IntentUtils.getParcelableExtra(intent, Intent.EXTRA_STREAM) if (uri == null) { - getString(R.string.share_intent_parsing_failed) + false } else { importFilesFromUris(listOf(uri), ::onImported) - null + true } } Intent.ACTION_SEND_MULTIPLE -> { @@ -50,20 +52,34 @@ class ExplorerActivityDrop : BaseExplorerActivity() { } if (uris != null) { importFilesFromUris(uris, ::onImported) - null + true } else { - getString(R.string.share_intent_parsing_failed) + false } } - else -> getString(R.string.share_intent_parsing_failed) + else -> false } + } else if ((intent.clipData?.itemCount ?: 0) > 0) { + val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(intent.clipData!!.getItemAt(0).text)) + val byteArray = ByteArray(byteBuffer.remaining()) + byteBuffer.get(byteArray) + val size = byteArray.size.toLong() + createNewFile { + var offset = 0L + while (offset < size) { + offset += encryptedVolume.write(it, offset, byteArray, offset, size-offset) + } + encryptedVolume.closeFile(it) + onImported() + } + true } else { - getString(R.string.share_intent_parsing_failed) + false } - errorMsg?.let { + if (!success) { CustomAlertDialogBuilder(this, theme) .setTitle(R.string.error) - .setMessage(it) + .setMessage(R.string.share_intent_parsing_failed) .setPositiveButton(R.string.ok, null) .show() } diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt index 173b826..562cb62 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt @@ -3,6 +3,7 @@ package sushi.hardcore.droidfs.util import android.content.ActivityNotFoundException import android.content.Context import android.net.Uri +import android.os.Build import android.os.Environment import android.os.storage.StorageManager import android.provider.DocumentsContract @@ -111,24 +112,27 @@ object PathUtils { } } Log.d(PATH_RESOLVER_TAG, "getExternalFilesDirs failed") - try { - val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() } - process.inputStream.readBytes().decodeToString().split("\n").forEach { line -> - if (line.startsWith("/dev/block/vold")) { - Log.d(PATH_RESOLVER_TAG, "mount: $line") - val fields = line.split(" ") - if (fields.size >= 3) { - val path = fields[2] - if (File(path).name == name) { - return path + // Don't risk to be killed by SELinux on newer Android versions + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + try { + val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() } + process.inputStream.readBytes().decodeToString().split("\n").forEach { line -> + if (line.startsWith("/dev/block/vold")) { + Log.d(PATH_RESOLVER_TAG, "mount: $line") + val fields = line.split(" ") + if (fields.size >= 3) { + val path = fields[2] + if (File(path).name == name) { + return path + } } } } + } catch (e: Exception) { + e.printStackTrace() } - } catch (e: Exception) { - e.printStackTrace() + Log.d(PATH_RESOLVER_TAG, "mount processing failed") } - Log.d(PATH_RESOLVER_TAG, "mount processing failed") return null } diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/UIUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/UIUtils.kt new file mode 100644 index 0000000..75a28a5 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/util/UIUtils.kt @@ -0,0 +1,42 @@ +package sushi.hardcore.droidfs.util + +import android.content.Context +import android.view.Menu +import android.widget.EditText +import androidx.core.content.ContextCompat +import sushi.hardcore.droidfs.R +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets +import java.util.* + +object UIUtils { + fun encodeEditTextContent(editText: EditText): ByteArray { + val charArray = CharArray(editText.text.length) + editText.text.getChars(0, editText.text.length, charArray, 0) + val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(charArray)) + Arrays.fill(charArray, Char.MIN_VALUE) + val byteArray = ByteArray(byteBuffer.remaining()) + byteBuffer.get(byteArray) + Wiper.wipe(byteBuffer) + return byteArray + } + + class MenuIconColor( + private val context: Context, + private val menu: Menu, + private val color: Int + ) { + fun applyTo(menuItemId: Int, drawableId: Int) { + menu.findItem(menuItemId)?.let { + it.icon = ContextCompat.getDrawable(context, drawableId)?.apply { + setTint(color) + } + } + } + } + + fun getMenuIconNeutralTint(context: Context, menu: Menu) = MenuIconColor( + context, menu, + ContextCompat.getColor(context, R.color.neutralIconTint), + ) +} diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt deleted file mode 100644 index 8003c72..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package sushi.hardcore.droidfs.util - -import android.widget.EditText -import java.nio.CharBuffer -import java.nio.charset.StandardCharsets -import java.util.* - -object WidgetUtil { - fun encodeEditTextContent(editText: EditText): ByteArray { - val charArray = CharArray(editText.text.length) - editText.text.getChars(0, editText.text.length, charArray, 0) - val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(charArray)) - Arrays.fill(charArray, Char.MIN_VALUE) - val byteArray = ByteArray(byteBuffer.remaining()) - byteBuffer.get(byteArray) - Wiper.wipe(byteBuffer) - return byteArray - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/widgets/DoubleTapPlayerView.kt b/app/src/main/java/sushi/hardcore/droidfs/widgets/DoubleTapPlayerView.kt index 7816702..13d1b9e 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/widgets/DoubleTapPlayerView.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/widgets/DoubleTapPlayerView.kt @@ -2,23 +2,13 @@ package sushi.hardcore.droidfs.widgets import android.annotation.SuppressLint import android.content.Context -import android.content.res.Configuration import android.media.session.PlaybackState import android.os.Handler import android.os.Looper import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.FrameLayout -import android.widget.LinearLayout -import androidx.core.view.GestureDetectorCompat -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding import androidx.media3.ui.PlayerView -import sushi.hardcore.droidfs.R class DoubleTapPlayerView @JvmOverloads constructor( context: Context, @@ -75,22 +65,7 @@ class DoubleTapPlayerView @JvmOverloads constructor( handler.postDelayed(stopDoubleTap, 700) } } - private val gestureDetector = GestureDetectorCompat(context, gestureListener) - private val density by lazy { - context.resources.displayMetrics.density - } - private val originalExoIconPaddingBottom by lazy { - resources.getDimension(R.dimen.exo_icon_padding_bottom) - } - private val originalExoIconSize by lazy { - resources.getDimension(R.dimen.exo_icon_size) - } - - init { - if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - handleOrientationChange(Configuration.ORIENTATION_LANDSCAPE) - } - } + private val gestureDetector = GestureDetector(context, gestureListener) @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { @@ -135,35 +110,4 @@ class DoubleTapPlayerView @JvmOverloads constructor( } } } - - private fun updateButtonSize(orientation: Int) { - val size = (if (orientation == Configuration.ORIENTATION_LANDSCAPE) 45*density else originalExoIconSize).toInt() - listOf(R.id.exo_prev, R.id.exo_rew_with_amount, R.id.exo_play_pause, R.id.exo_ffwd_with_amount, R.id.exo_next).forEach { - findViewById(it).updateLayoutParams { - width = size - height = size - } - } - // fix text vertical alignment inside icons - val paddingBottom = (if (orientation == Configuration.ORIENTATION_LANDSCAPE) 15*density else originalExoIconPaddingBottom).toInt() - listOf(R.id.exo_rew_with_amount, R.id.exo_ffwd_with_amount).forEach { - findViewById