diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt b/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt index 18c57c89..e4b792d3 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/Extensions.kt @@ -44,9 +44,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.files.FileFilter -import com.afollestad.materialdialogs.files.fileChooser -import com.afollestad.materialdialogs.files.folderChooser import com.amaze.fileutilities.audio_player.AudioPlayerService import com.amaze.fileutilities.home_page.database.BlurAnalysis import com.amaze.fileutilities.home_page.database.BlurAnalysisDao @@ -62,6 +59,9 @@ import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysis import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysisDao import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysisMetadata import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysisMetadataDao +import com.amaze.fileutilities.utilis.dialog_picker.FileFilter +import com.amaze.fileutilities.utilis.dialog_picker.fileChooser +import com.amaze.fileutilities.utilis.dialog_picker.folderChooser import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ContextExt.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ContextExt.kt new file mode 100644 index 00000000..fc1115e9 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ContextExt.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.content.Context +import java.io.File + +internal fun Context.getExternalFilesDir(): File? { + return this.getExternalFilesDir(null) +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFileChooserExt.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFileChooserExt.kt new file mode 100644 index 00000000..65c992a6 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFileChooserExt.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.annotation.SuppressLint +import android.content.Context +import android.text.InputFilter +import android.widget.EditText +import android.widget.TextView +import androidx.annotation.CheckResult +import androidx.annotation.StringRes +import androidx.recyclerview.widget.LinearLayoutManager +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.WhichButton.POSITIVE +import com.afollestad.materialdialogs.actions.setActionButtonEnabled +import com.afollestad.materialdialogs.customview.customView +import com.afollestad.materialdialogs.customview.getCustomView +import com.afollestad.materialdialogs.files.R +import com.afollestad.materialdialogs.input.getInputField +import com.afollestad.materialdialogs.input.input +import com.afollestad.materialdialogs.internal.list.DialogRecyclerView +import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor +import java.io.File + +typealias FileFilter = ((File) -> Boolean)? +typealias FileCallback = ((dialog: MaterialDialog, file: File) -> Unit)? + +/** Gets the selected file for the current file chooser dialog. */ +@CheckResult +fun MaterialDialog.selectedFile(): File? { + val customView = getCustomView() + val list: DialogRecyclerView = customView.findViewById(R.id.list) + return (list.adapter as? FileChooserAdapter)?.selectedFile +} + +/** + * Shows a dialog that lets the user select a local file. + * + * @param initialDirectory The directory that is listed initially, defaults to external storage. + * @param filter A filter to apply when listing files, defaults to only show non-hidden files. + * @param waitForPositiveButton When true, the callback isn't invoked until the user selects a + * file and taps on the positive action button. Defaults to true if the dialog has buttons. + * @param emptyTextRes A string resource displayed on the empty view shown when a directory is + * empty. Defaults to "This folder's empty!". + * @param selection A callback invoked when a file is selected. + */ +@SuppressLint("CheckResult") +fun MaterialDialog.fileChooser( + context: Context, + initialDirectory: File? = context.getExternalFilesDir(), + filter: FileFilter = null, + waitForPositiveButton: Boolean = true, + emptyTextRes: Int = R.string.files_default_empty_text, + allowFolderCreation: Boolean = false, + @StringRes folderCreationLabel: Int? = null, + selection: FileCallback = null +): MaterialDialog { + var actualFilter: FileFilter = filter + + if (allowFolderCreation) { + // we already have permissions at app startup +// check(hasWriteStoragePermission()) { +// "You must have the WRITE_EXTERNAL_STORAGE permission first." +// } + if (filter == null) { + actualFilter = { !it.isHidden && it.canWrite() } + } + } else { + // we already have permissions at app startup +// check(hasWriteStoragePermission()) { +// "You must have the WRITE_EXTERNAL_STORAGE permission first." +// } + if (filter == null) { + actualFilter = { !it.isHidden && it.canRead() } + } + } + + check(initialDirectory != null) { + "The initial directory is null." + } + + customView(R.layout.md_file_chooser_base, noVerticalPadding = true) + setActionButtonEnabled(POSITIVE, false) + + val customView = getCustomView() + val list: DialogRecyclerView = customView.findViewById(R.id.list) + val emptyText: TextView = customView.findViewById(R.id.empty_text) + emptyText.setText(emptyTextRes) + emptyText.maybeSetTextColor(windowContext, R.attr.md_color_content) + + list.attach(this) + list.layoutManager = LinearLayoutManager(windowContext) + val adapter = FileChooserAdapter( + dialog = this, + initialFolder = initialDirectory, + waitForPositiveButton = waitForPositiveButton, + emptyView = emptyText, + onlyFolders = false, + filter = actualFilter, + allowFolderCreation = allowFolderCreation, + folderCreationLabel = folderCreationLabel, + callback = selection + ) + list.adapter = adapter + + if (waitForPositiveButton && selection != null) { + setActionButtonEnabled(POSITIVE, false) + positiveButton { + val selectedFile = adapter.selectedFile + if (selectedFile != null) { + selection.invoke(this, selectedFile) + } + } + } + + return this +} + +internal fun MaterialDialog.showNewFolderCreator( + parent: File, + @StringRes folderCreationLabel: Int?, + onCreation: () -> Unit +) { + val dialog = MaterialDialog(windowContext).show { + title(folderCreationLabel ?: R.string.files_new_folder) + input(hintRes = R.string.files_new_folder_hint) { _, input -> + File(parent, input.toString().trim()).mkdir() + onCreation() + } + } + dialog.getInputField() + .blockReservedCharacters() +} + +private fun EditText.blockReservedCharacters() { + filters += InputFilter { source, _, _, _, _, _ -> + if (source.isEmpty()) { + return@InputFilter null + } + val last = source[source.length - 1] + val reservedChars = "?:\"*|/\\<>" + if (reservedChars.indexOf(last) > -1) { + source.subSequence(0, source.length - 1) + } else { + null + } + } +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFolderChooserExt.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFolderChooserExt.kt new file mode 100644 index 00000000..086a826b --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/DialogFolderChooserExt.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.TextView +import androidx.annotation.CheckResult +import androidx.annotation.StringRes +import androidx.recyclerview.widget.LinearLayoutManager +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.WhichButton.POSITIVE +import com.afollestad.materialdialogs.actions.setActionButtonEnabled +import com.afollestad.materialdialogs.customview.customView +import com.afollestad.materialdialogs.customview.getCustomView +import com.afollestad.materialdialogs.files.R +import com.afollestad.materialdialogs.internal.list.DialogRecyclerView +import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor +import java.io.File + +/** Gets the selected folder for the current folder chooser dialog. */ +@CheckResult +fun MaterialDialog.selectedFolder(): File? { + val list: DialogRecyclerView = getCustomView().findViewById(R.id.list) + return (list.adapter as? FileChooserAdapter)?.selectedFile +} + +/** + * Shows a dialog that lets the user select a local folder. + * + * @param initialDirectory The directory that is listed initially, defaults to external storage. + * @param filter A filter to apply when listing folders, defaults to only show non-hidden folders. + * @param waitForPositiveButton When true, the callback isn't invoked until the user selects a + * folder and taps on the positive action button. Defaults to true if the dialog has buttons. + * @param emptyTextRes A string resource displayed on the empty view shown when a directory is + * empty. Defaults to "This folder's empty!". + * @param selection A callback invoked when a folder is selected. + */ +@SuppressLint("CheckResult") +fun MaterialDialog.folderChooser( + context: Context, + initialDirectory: File? = context.getExternalFilesDir(), + filter: FileFilter = null, + waitForPositiveButton: Boolean = true, + emptyTextRes: Int = R.string.files_default_empty_text, + allowFolderCreation: Boolean = false, + @StringRes folderCreationLabel: Int? = null, + selection: FileCallback = null +): MaterialDialog { + var actualFilter: FileFilter = filter + + if (allowFolderCreation) { + // we already have permissions at app startup +// check(hasWriteStoragePermission()) { +// "You must have the WRITE_EXTERNAL_STORAGE permission first." +// } + if (filter == null) { + actualFilter = { !it.isHidden && it.canWrite() } + } + } else { + // we already have permissions at app startup +// check(hasWriteStoragePermission()) { +// "You must have the READ_EXTERNAL_STORAGE permission first." +// } + if (filter == null) { + actualFilter = { !it.isHidden && it.canRead() } + } + } + + check(initialDirectory != null) { + "The initial directory is null." + } + + customView(R.layout.md_file_chooser_base, noVerticalPadding = true) + setActionButtonEnabled(POSITIVE, false) + + val customView = getCustomView() + val list: DialogRecyclerView = customView.findViewById(R.id.list) + val emptyText: TextView = customView.findViewById(R.id.empty_text) + emptyText.setText(emptyTextRes) + emptyText.maybeSetTextColor(windowContext, R.attr.md_color_content) + + list.attach(this) + list.layoutManager = LinearLayoutManager(windowContext) + + val adapter = FileChooserAdapter( + dialog = this, + initialFolder = initialDirectory, + waitForPositiveButton = waitForPositiveButton, + emptyView = emptyText, + onlyFolders = true, + filter = actualFilter, + allowFolderCreation = allowFolderCreation, + folderCreationLabel = folderCreationLabel, + callback = selection + ) + list.adapter = adapter + + if (waitForPositiveButton && selection != null) { + positiveButton { + val selectedFile = adapter.selectedFile + if (selectedFile != null) { + selection.invoke(this, selectedFile) + } + } + } + + return this +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FileChooserAdapter.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FileChooserAdapter.kt new file mode 100644 index 00000000..61786563 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FileChooserAdapter.kt @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.WhichButton.POSITIVE +import com.afollestad.materialdialogs.actions.hasActionButtons +import com.afollestad.materialdialogs.actions.setActionButtonEnabled +import com.afollestad.materialdialogs.callbacks.onDismiss +import com.afollestad.materialdialogs.files.R +import com.afollestad.materialdialogs.list.getItemSelector +import com.afollestad.materialdialogs.utils.MDUtil.isColorDark +import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor +import com.afollestad.materialdialogs.utils.MDUtil.resolveColor +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.util.Locale + +internal class FileChooserViewHolder( + itemView: View, + private val adapter: FileChooserAdapter +) : RecyclerView.ViewHolder(itemView), OnClickListener { + + init { + itemView.setOnClickListener(this) + } + + val iconView: ImageView = itemView.findViewById(R.id.icon) + val nameView: TextView = itemView.findViewById(R.id.name) + + override fun onClick(view: View) = adapter.itemClicked(adapterPosition) +} + +/** @author Aidan Follestad (afollestad */ +internal class FileChooserAdapter( + private val dialog: MaterialDialog, + initialFolder: File, + private val waitForPositiveButton: Boolean, + private val emptyView: TextView, + private val onlyFolders: Boolean, + private val filter: FileFilter, + private val allowFolderCreation: Boolean, + @StringRes private val folderCreationLabel: Int?, + private val callback: FileCallback +) : RecyclerView.Adapter() { + + var selectedFile: File? = null + + private var currentFolder = initialFolder + private var listingJob: Job? = null + private var contents: List? = null + + private val isLightTheme = + resolveColor(dialog.windowContext, attr = android.R.attr.textColorPrimary).isColorDark() + + init { + dialog.onDismiss { listingJob?.cancel() } + switchDirectory(initialFolder) + } + + fun itemClicked(index: Int) { + val parent = currentFolder.betterParent(dialog.context, allowFolderCreation, filter) + if (parent != null && index == goUpIndex()) { + // go up + switchDirectory(parent) + return + } else if (currentFolder.canWrite() && allowFolderCreation && index == newFolderIndex()) { + // New folder + dialog.showNewFolderCreator( + parent = currentFolder, + folderCreationLabel = folderCreationLabel + ) { + // Refresh view + switchDirectory(currentFolder) + } + return + } + + val actualIndex = actualIndex(index) + val selected = contents!![actualIndex].jumpOverEmulated(dialog.context) + + if (selected.isDirectory) { + switchDirectory(selected) + } else { + val previousSelectedIndex = getSelectedIndex() + this.selectedFile = selected + val actualWaitForPositive = waitForPositiveButton && dialog.hasActionButtons() + + if (actualWaitForPositive) { + dialog.setActionButtonEnabled(POSITIVE, true) + notifyItemChanged(index) + notifyItemChanged(previousSelectedIndex) + } else { + callback?.invoke(dialog, selected) + dialog.dismiss() + } + } + } + + private fun switchDirectory(directory: File) { + listingJob?.cancel() + listingJob = GlobalScope.launch(Main) { + if (onlyFolders) { + selectedFile = directory + dialog.setActionButtonEnabled(POSITIVE, true) + } + + currentFolder = directory + dialog.title(text = directory.friendlyName(dialog.context)) + + val result = withContext(IO) { + val rawContents = directory.listFiles() ?: emptyArray() + if (onlyFolders) { + rawContents + .filter { it.isDirectory && filter?.invoke(it) ?: true } + .sortedBy { it.name.toLowerCase(Locale.getDefault()) } + } else { + rawContents + .filter { filter?.invoke(it) ?: true } + .sortedWith( + compareBy({ !it.isDirectory }, { + it.nameWithoutExtension.toLowerCase(Locale.getDefault()) + }) + ) + } + } + + contents = result.apply { + emptyView.setVisible(isEmpty()) + } + notifyDataSetChanged() + } + } + + override fun getItemCount(): Int { + var count = contents?.size ?: 0 + if (currentFolder.hasParent(dialog.context, allowFolderCreation, filter)) { + count += 1 + } + if (allowFolderCreation && currentFolder.canWrite()) { + count += 1 + } + return count + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): FileChooserViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.md_file_chooser_item, parent, false) + view.background = dialog.getItemSelector() + + val viewHolder = FileChooserViewHolder(view, this) + viewHolder.nameView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content) + return viewHolder + } + + override fun onBindViewHolder( + holder: FileChooserViewHolder, + position: Int + ) { + val currentParent = currentFolder.betterParent(dialog.context, allowFolderCreation, filter) + if (currentParent != null && position == goUpIndex()) { + // Go up + holder.iconView.setImageResource( + if (isLightTheme) R.drawable.icon_return_dark + else R.drawable.icon_return_light + ) + holder.nameView.text = currentParent.name + holder.itemView.isActivated = false + return + } + + if (allowFolderCreation && currentFolder.canWrite() && position == newFolderIndex()) { + // New folder + holder.iconView.setImageResource( + if (isLightTheme) R.drawable.icon_new_folder_dark + else R.drawable.icon_new_folder_light + ) + holder.nameView.text = dialog.windowContext.getString( + folderCreationLabel ?: R.string.files_new_folder + ) + holder.itemView.isActivated = false + return + } + + val actualIndex = actualIndex(position) + val item = contents!![actualIndex] + holder.iconView.setImageResource(item.iconRes()) + holder.nameView.text = item.name + holder.itemView.isActivated = selectedFile?.absolutePath == item.absolutePath ?: false + } + + private fun goUpIndex() = if (currentFolder.hasParent( + dialog.context, allowFolderCreation, + filter + ) + ) 0 else -1 + + private fun newFolderIndex() = if (currentFolder.hasParent( + dialog.context, allowFolderCreation, + filter + ) + ) 1 else 0 + + private fun actualIndex(position: Int): Int { + var actualIndex = position + if (currentFolder.hasParent(dialog.context, allowFolderCreation, filter)) { + actualIndex -= 1 + } + if (currentFolder.canWrite() && allowFolderCreation) { + actualIndex -= 1 + } + return actualIndex + } + + private fun File.iconRes(): Int { + return if (isLightTheme) { + if (this.isDirectory) R.drawable.icon_folder_dark + else R.drawable.icon_file_dark + } else { + if (this.isDirectory) R.drawable.icon_folder_light + else R.drawable.icon_file_light + } + } + + private fun getSelectedIndex(): Int { + if (selectedFile == null) return -1 + else if (contents?.isEmpty() == true) return -1 + val index = contents?.indexOfFirst { it.absolutePath == selectedFile?.absolutePath } ?: -1 + return if (index > -1 && currentFolder.hasParent( + dialog.context, allowFolderCreation, + filter + ) + ) index + 1 else index + } +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FilesUtilExt.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FilesUtilExt.kt new file mode 100644 index 00000000..ec6ea53e --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/FilesUtilExt.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("SpellCheckingInspection") + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.Manifest.permission +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.files.FileFilter +import java.io.File + +internal fun File.hasParent( + context: Context, + writeable: Boolean, + filter: FileFilter +) = betterParent(context, writeable, filter) != null + +internal fun File.isExternalStorage(context: Context) = + absolutePath == context.getExternalFilesDir()?.absolutePath + +internal fun File.isRoot() = absolutePath == "/" + +internal fun File.betterParent( + context: Context, + writeable: Boolean, + filter: FileFilter +): File? { + val parentToUse = ( + if (isExternalStorage(context)) { + // Emulated external storage's parent is empty so jump over it + context.getExternalFilesDir()?.parentFile?.parentFile + } else { + parentFile + } + ) ?: return null + + if ((writeable && !parentToUse.canWrite()) || !parentToUse.canRead()) { + // We can't access this folder + return null + } + + val folderContent = + parentToUse.listFiles()?.filter { filter?.invoke(it) ?: true } ?: emptyList() + if (folderContent.isEmpty()) { + // There is nothing in this folder most likely because we can't access files inside of it. + // We don't want to get stuck here. + return null + } + + return parentToUse +} + +internal fun File.jumpOverEmulated(context: Context): File { + val externalFileDir = context.getExternalFilesDir() + externalFileDir?.parentFile?.let { externalParentFile -> + if (absolutePath == externalParentFile.absolutePath) { + return externalFileDir + } + } + return this +} + +internal fun File.friendlyName(context: Context) = when { + isExternalStorage(context) -> "External Storage" + isRoot() -> "Root" + else -> name +} + +internal fun Context.hasPermission(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == + PackageManager.PERMISSION_GRANTED +} + +internal fun MaterialDialog.hasReadStoragePermission(): Boolean { + return windowContext.hasPermission(permission.READ_EXTERNAL_STORAGE) +} + +internal fun MaterialDialog.hasWriteStoragePermission(): Boolean { + return windowContext.hasPermission(permission.WRITE_EXTERNAL_STORAGE) +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ViewExt.kt b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ViewExt.kt new file mode 100644 index 00000000..56443fd1 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/dialog_picker/ViewExt.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amaze.fileutilities.utilis.dialog_picker + +import android.view.View + +internal fun T.setVisible(visible: Boolean) { + visibility = if (visible) View.VISIBLE else View.INVISIBLE +} diff --git a/app/src/main/java/com/amaze/fileutilities/video_player/BaseVideoPlayerActivity.kt b/app/src/main/java/com/amaze/fileutilities/video_player/BaseVideoPlayerActivity.kt index a5498727..4ad39931 100644 --- a/app/src/main/java/com/amaze/fileutilities/video_player/BaseVideoPlayerActivity.kt +++ b/app/src/main/java/com/amaze/fileutilities/video_player/BaseVideoPlayerActivity.kt @@ -62,7 +62,6 @@ import androidx.core.view.updateLayoutParams import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.afollestad.materialdialogs.files.FileFilter import com.amaze.fileutilities.PermissionsActivity import com.amaze.fileutilities.R import com.amaze.fileutilities.audio_player.AudioPlayerService @@ -75,6 +74,7 @@ import com.amaze.fileutilities.home_page.ui.transfer.TransferFragment import com.amaze.fileutilities.utilis.PreferencesConstants import com.amaze.fileutilities.utilis.Utils import com.amaze.fileutilities.utilis.Utils.Companion.showProcessingDialog +import com.amaze.fileutilities.utilis.dialog_picker.FileFilter import com.amaze.fileutilities.utilis.getAppCommonSharedPreferences import com.amaze.fileutilities.utilis.getExternalStorageDirectory import com.amaze.fileutilities.utilis.getFileFromUri diff --git a/gradle.properties b/gradle.properties index b54dc1e4..40378899 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ kotlin.code.style=official org.gradle.parallel=true abiFilters=x86;x86_64;armeabi-v7a;arm64-v8a # for macs, omit for other operating systems -#org.gradle.java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home +org.gradle.java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home # https://github.com/usefulness/easylauncher-gradle-plugin/issues/408 android.disableResourceValidation=true \ No newline at end of file