812 lines
31 KiB
Kotlin
812 lines
31 KiB
Kotlin
/*
|
|
* Copyright (c) 2020 Hemanth Savarala.
|
|
*
|
|
* Licensed under the GNU General Public License v3
|
|
*
|
|
* This 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 software 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.
|
|
*/
|
|
package code.name.monkey.retromusic.fragments.folder
|
|
|
|
import android.app.Dialog
|
|
import android.content.Context
|
|
import android.media.MediaScannerConnection
|
|
import android.os.Bundle
|
|
import android.os.Environment
|
|
import android.text.Html
|
|
import android.view.*
|
|
import android.webkit.MimeTypeMap
|
|
import android.widget.PopupMenu
|
|
import android.widget.Toast
|
|
import androidx.activity.OnBackPressedCallback
|
|
import androidx.loader.app.LoaderManager
|
|
import androidx.loader.content.Loader
|
|
import androidx.navigation.Navigation.findNavController
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import code.name.monkey.appthemehelper.ThemeStore.Companion.accentColor
|
|
import code.name.monkey.appthemehelper.common.ATHToolbarActivity
|
|
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
|
|
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
|
import code.name.monkey.retromusic.App
|
|
import code.name.monkey.retromusic.R
|
|
import code.name.monkey.retromusic.adapter.SongFileAdapter
|
|
import code.name.monkey.retromusic.adapter.Storage
|
|
import code.name.monkey.retromusic.adapter.StorageAdapter
|
|
import code.name.monkey.retromusic.adapter.StorageClickListener
|
|
import code.name.monkey.retromusic.databinding.FragmentFolderBinding
|
|
import code.name.monkey.retromusic.extensions.drawNextToNavbar
|
|
import code.name.monkey.retromusic.extensions.surfaceColor
|
|
import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment
|
|
import code.name.monkey.retromusic.fragments.folder.FoldersFragment.ListPathsAsyncTask.OnPathsListedCallback
|
|
import code.name.monkey.retromusic.fragments.folder.FoldersFragment.ListSongsAsyncTask.OnSongsListedCallback
|
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote.openQueue
|
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote.playingQueue
|
|
import code.name.monkey.retromusic.helper.menu.SongMenuHelper.handleMenuClick
|
|
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
|
|
import code.name.monkey.retromusic.interfaces.ICabCallback
|
|
import code.name.monkey.retromusic.interfaces.ICabHolder
|
|
import code.name.monkey.retromusic.interfaces.ICallbacks
|
|
import code.name.monkey.retromusic.interfaces.IMainActivityFragmentCallbacks
|
|
import code.name.monkey.retromusic.misc.DialogAsyncTask
|
|
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener
|
|
import code.name.monkey.retromusic.misc.WrappedAsyncTaskLoader
|
|
import code.name.monkey.retromusic.model.Song
|
|
import code.name.monkey.retromusic.providers.BlacklistStore
|
|
import code.name.monkey.retromusic.util.*
|
|
import code.name.monkey.retromusic.util.DensityUtil.dip2px
|
|
import code.name.monkey.retromusic.util.PreferenceUtil.startDirectory
|
|
import code.name.monkey.retromusic.util.ThemedFastScroller.create
|
|
import code.name.monkey.retromusic.views.BreadCrumbLayout.Crumb
|
|
import code.name.monkey.retromusic.views.BreadCrumbLayout.SelectionCallback
|
|
import code.name.monkey.retromusic.views.ScrollingViewOnApplyWindowInsetsListener
|
|
import com.afollestad.materialcab.attached.AttachedCab
|
|
import com.afollestad.materialcab.attached.destroy
|
|
import com.afollestad.materialcab.attached.isActive
|
|
import com.afollestad.materialcab.createCab
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
import com.google.android.material.shape.MaterialShapeDrawable
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import com.google.android.material.transition.MaterialSharedAxis
|
|
import java.io.*
|
|
import java.lang.ref.WeakReference
|
|
import java.util.*
|
|
|
|
class FoldersFragment : AbsMainActivityFragment(R.layout.fragment_folder),
|
|
IMainActivityFragmentCallbacks, ICabHolder, SelectionCallback, ICallbacks,
|
|
LoaderManager.LoaderCallbacks<List<File>>, StorageClickListener {
|
|
private var _binding: FragmentFolderBinding? = null
|
|
private val binding get() = _binding!!
|
|
private var adapter: SongFileAdapter? = null
|
|
private var storageAdapter: StorageAdapter? = null
|
|
private var cab: AttachedCab? = null
|
|
private val fileComparator = Comparator { lhs: File, rhs: File ->
|
|
if (lhs.isDirectory && !rhs.isDirectory) {
|
|
return@Comparator -1
|
|
} else if (!lhs.isDirectory && rhs.isDirectory) {
|
|
return@Comparator 1
|
|
} else {
|
|
return@Comparator lhs.name.compareTo(rhs.name, ignoreCase = true)
|
|
}
|
|
}
|
|
private var storageItems = ArrayList<Storage>()
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
): View {
|
|
_binding = FragmentFolderBinding.inflate(inflater, container, false)
|
|
return binding.root
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
mainActivity.addMusicServiceEventListener(libraryViewModel)
|
|
mainActivity.setSupportActionBar(binding.toolbar)
|
|
mainActivity.supportActionBar?.title = null
|
|
setUpBreadCrumbs()
|
|
setUpRecyclerView()
|
|
setUpAdapter()
|
|
setUpTitle()
|
|
requireActivity().onBackPressedDispatcher.addCallback(
|
|
viewLifecycleOwner,
|
|
object : OnBackPressedCallback(true) {
|
|
override fun handleOnBackPressed() {
|
|
if (!handleBackPress()) {
|
|
remove()
|
|
requireActivity().onBackPressed()
|
|
}
|
|
}
|
|
})
|
|
binding.toolbarContainer.drawNextToNavbar()
|
|
binding.appBarLayout.statusBarForeground =
|
|
MaterialShapeDrawable.createWithElevationOverlay(requireContext())
|
|
}
|
|
|
|
private fun setUpTitle() {
|
|
binding.toolbar.setNavigationOnClickListener { v: View? ->
|
|
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).setDuration(300)
|
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).setDuration(300)
|
|
findNavController(v!!).navigate(R.id.searchFragment, null, navOptions)
|
|
}
|
|
binding.appNameText.text = resources.getString(R.string.folders)
|
|
}
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
setHasOptionsMenu(true)
|
|
if (savedInstanceState == null) {
|
|
switchToFileAdapter()
|
|
setCrumb(
|
|
Crumb(
|
|
FileUtil.safeGetCanonicalFile(startDirectory)
|
|
),
|
|
true
|
|
)
|
|
} else {
|
|
binding.breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS))
|
|
LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this)
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
saveScrollPosition()
|
|
}
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
super.onSaveInstanceState(outState)
|
|
if (_binding != null) {
|
|
outState.putParcelable(CRUMBS, binding.breadCrumbs.stateWrapper)
|
|
}
|
|
}
|
|
|
|
override fun handleBackPress(): Boolean {
|
|
if (cab != null && cab!!.isActive()) {
|
|
cab?.destroy()
|
|
return true
|
|
}
|
|
if (binding.breadCrumbs.popHistory()) {
|
|
setCrumb(binding.breadCrumbs.lastHistory(), false)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<File>> {
|
|
return AsyncFileLoader(this)
|
|
}
|
|
|
|
override fun onCrumbSelection(crumb: Crumb, index: Int) {
|
|
setCrumb(crumb, true)
|
|
}
|
|
|
|
override fun onFileMenuClicked(file: File, view: View) {
|
|
val popupMenu = PopupMenu(activity, view)
|
|
if (file.isDirectory) {
|
|
popupMenu.inflate(R.menu.menu_item_directory)
|
|
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
|
|
when (val itemId = item.itemId) {
|
|
R.id.action_play_next, R.id.action_add_to_current_playing, R.id.action_add_to_playlist, R.id.action_delete_from_device -> {
|
|
ListSongsAsyncTask(
|
|
activity,
|
|
null,
|
|
object : OnSongsListedCallback {
|
|
override fun onSongsListed(songs: List<Song>, extra: Any?) {
|
|
if (songs.isNotEmpty()) {
|
|
SongsMenuHelper.handleMenuClick(
|
|
requireActivity(), songs, itemId
|
|
)
|
|
}
|
|
}
|
|
})
|
|
.execute(
|
|
ListSongsAsyncTask.LoadingInfo(
|
|
toList(file), AUDIO_FILE_FILTER, fileComparator
|
|
)
|
|
)
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
R.id.action_add_to_blacklist -> {
|
|
BlacklistStore.getInstance(App.getContext()).addPath(file)
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
R.id.action_set_as_start_directory -> {
|
|
startDirectory = file
|
|
Toast.makeText(
|
|
activity,
|
|
String.format(getString(R.string.new_start_directory), file.path),
|
|
Toast.LENGTH_SHORT
|
|
)
|
|
.show()
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
R.id.action_scan -> {
|
|
ListPathsAsyncTask(
|
|
activity,
|
|
object : OnPathsListedCallback {
|
|
override fun onPathsListed(paths: Array<String?>) {
|
|
scanPaths(paths)
|
|
}
|
|
})
|
|
.execute(ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER))
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
}
|
|
false
|
|
}
|
|
} else {
|
|
popupMenu.inflate(R.menu.menu_item_file)
|
|
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
|
|
when (val itemId = item.itemId) {
|
|
R.id.action_play_next, R.id.action_add_to_current_playing, R.id.action_add_to_playlist, R.id.action_go_to_album, R.id.action_go_to_artist, R.id.action_share, R.id.action_tag_editor, R.id.action_details, R.id.action_set_as_ringtone, R.id.action_delete_from_device -> {
|
|
ListSongsAsyncTask(
|
|
activity,
|
|
null,
|
|
object : OnSongsListedCallback {
|
|
override fun onSongsListed(songs: List<Song>, extra: Any?) {
|
|
handleMenuClick(
|
|
requireActivity(), songs[0], itemId
|
|
)
|
|
}
|
|
})
|
|
.execute(
|
|
ListSongsAsyncTask.LoadingInfo(
|
|
toList(file), AUDIO_FILE_FILTER, fileComparator
|
|
)
|
|
)
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
R.id.action_scan -> {
|
|
ListPathsAsyncTask(
|
|
activity,
|
|
object : OnPathsListedCallback {
|
|
override fun onPathsListed(paths: Array<String?>) {
|
|
scanPaths(paths)
|
|
}
|
|
})
|
|
.execute(ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER))
|
|
return@setOnMenuItemClickListener true
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
popupMenu.show()
|
|
}
|
|
|
|
override fun onFileSelected(file: File) {
|
|
var mFile = file
|
|
mFile = tryGetCanonicalFile(mFile) // important as we compare the path value later
|
|
if (mFile.isDirectory) {
|
|
setCrumb(Crumb(mFile), true)
|
|
} else {
|
|
val fileFilter = FileFilter { pathname: File ->
|
|
!pathname.isDirectory && AUDIO_FILE_FILTER.accept(pathname)
|
|
}
|
|
ListSongsAsyncTask(
|
|
activity,
|
|
mFile,
|
|
object : OnSongsListedCallback {
|
|
override fun onSongsListed(songs: List<Song>, extra: Any?) {
|
|
val file1 = extra as File
|
|
var startIndex = -1
|
|
for (i in songs.indices) {
|
|
if (file1
|
|
.path
|
|
== songs[i].data
|
|
) { // path is already canonical here
|
|
startIndex = i
|
|
break
|
|
}
|
|
}
|
|
if (startIndex > -1) {
|
|
openQueue(songs, startIndex, true)
|
|
} else {
|
|
Snackbar.make(
|
|
binding.root,
|
|
Html.fromHtml(
|
|
String.format(
|
|
getString(R.string.not_listed_in_media_store), file1.name
|
|
)
|
|
),
|
|
Snackbar.LENGTH_LONG
|
|
)
|
|
.setAction(
|
|
R.string.action_scan
|
|
) {
|
|
ListPathsAsyncTask(
|
|
requireActivity(),
|
|
object : OnPathsListedCallback {
|
|
override fun onPathsListed(paths: Array<String?>) {
|
|
scanPaths(paths)
|
|
}
|
|
})
|
|
.execute(
|
|
ListPathsAsyncTask.LoadingInfo(
|
|
file1, AUDIO_FILE_FILTER
|
|
)
|
|
)
|
|
}
|
|
.setActionTextColor(accentColor(requireActivity()))
|
|
.show()
|
|
}
|
|
}
|
|
})
|
|
.execute(
|
|
ListSongsAsyncTask.LoadingInfo(
|
|
toList(mFile.parentFile), fileFilter, fileComparator
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun onLoadFinished(loader: Loader<List<File>>, data: List<File>) {
|
|
updateAdapter(data)
|
|
}
|
|
|
|
override fun onLoaderReset(loader: Loader<List<File>>) {
|
|
updateAdapter(LinkedList())
|
|
}
|
|
|
|
override fun onMultipleItemAction(item: MenuItem, files: ArrayList<File>) {
|
|
val itemId = item.itemId
|
|
ListSongsAsyncTask(
|
|
activity,
|
|
null,
|
|
object : OnSongsListedCallback {
|
|
override fun onSongsListed(songs: List<Song>, extra: Any?) {
|
|
SongsMenuHelper.handleMenuClick(
|
|
requireActivity(),
|
|
songs,
|
|
itemId
|
|
)
|
|
}
|
|
})
|
|
.execute(ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, fileComparator))
|
|
}
|
|
|
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
super.onPrepareOptionsMenu(menu)
|
|
ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), binding.toolbar)
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
super.onCreateOptionsMenu(menu, inflater)
|
|
menu.add(0, R.id.action_scan, 0, R.string.scan_media)
|
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
|
menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory)
|
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
|
menu.removeItem(R.id.action_grid_size)
|
|
menu.removeItem(R.id.action_layout_type)
|
|
menu.removeItem(R.id.action_sort_order)
|
|
ToolbarContentTintHelper.handleOnCreateOptionsMenu(
|
|
requireContext(), binding.toolbar, menu, ATHToolbarActivity.getToolbarBackgroundColor(
|
|
binding.toolbar
|
|
)
|
|
)
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
when (item.itemId) {
|
|
R.id.action_go_to_start_directory -> {
|
|
setCrumb(
|
|
Crumb(
|
|
tryGetCanonicalFile(startDirectory)
|
|
),
|
|
true
|
|
)
|
|
return true
|
|
}
|
|
R.id.action_scan -> {
|
|
val crumb = activeCrumb
|
|
if (crumb != null) {
|
|
ListPathsAsyncTask(
|
|
activity,
|
|
object : OnPathsListedCallback {
|
|
override fun onPathsListed(paths: Array<String?>) {
|
|
scanPaths(paths)
|
|
}
|
|
})
|
|
.execute(ListPathsAsyncTask.LoadingInfo(crumb.file, AUDIO_FILE_FILTER))
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return super.onOptionsItemSelected(item)
|
|
}
|
|
|
|
override fun onQueueChanged() {
|
|
super.onQueueChanged()
|
|
checkForPadding()
|
|
}
|
|
|
|
override fun onServiceConnected() {
|
|
super.onServiceConnected()
|
|
checkForPadding()
|
|
}
|
|
|
|
override fun openCab(menuRes: Int, callback: ICabCallback): AttachedCab {
|
|
if (cab != null && cab!!.isActive()) {
|
|
cab?.destroy()
|
|
}
|
|
cab = createCab(R.id.toolbar_container) {
|
|
menu(menuRes)
|
|
closeDrawable(R.drawable.ic_close)
|
|
backgroundColor(literal = RetroColorUtil.shiftBackgroundColor(surfaceColor()))
|
|
slideDown()
|
|
onCreate { cab, menu -> callback.onCabCreated(cab, menu) }
|
|
onSelection {
|
|
callback.onCabItemClicked(it)
|
|
}
|
|
onDestroy { callback.onCabFinished(it) }
|
|
}
|
|
return cab as AttachedCab
|
|
}
|
|
|
|
private fun checkForPadding() {
|
|
val count = adapter?.itemCount ?: 0
|
|
if (_binding != null) {
|
|
val params = binding.root.layoutParams as ViewGroup.MarginLayoutParams
|
|
params.bottomMargin = if (count > 0 && playingQueue.isNotEmpty()) dip2px(
|
|
requireContext(),
|
|
104f
|
|
) else dip2px(requireContext(), 54f)
|
|
binding.root.layoutParams = params
|
|
}
|
|
}
|
|
|
|
private fun checkIsEmpty() {
|
|
if (_binding != null) {
|
|
binding.emptyEmoji.text = getEmojiByUnicode(0x1F631)
|
|
binding.empty.visibility =
|
|
if (adapter == null || adapter!!.itemCount == 0) View.VISIBLE else View.GONE
|
|
}
|
|
}
|
|
|
|
private val activeCrumb: Crumb?
|
|
get() = if (_binding != null) {
|
|
if (binding.breadCrumbs.size() > 0) binding.breadCrumbs.getCrumb(binding.breadCrumbs.activeIndex) else null
|
|
} else null
|
|
|
|
private fun getEmojiByUnicode(unicode: Int): String {
|
|
return String(Character.toChars(unicode))
|
|
}
|
|
|
|
private fun saveScrollPosition() {
|
|
val crumb = activeCrumb
|
|
if (crumb != null) {
|
|
crumb.scrollPosition =
|
|
(binding.recyclerView.layoutManager as LinearLayoutManager?)!!.findFirstVisibleItemPosition()
|
|
}
|
|
}
|
|
|
|
private fun scanPaths(toBeScanned: Array<String?>) {
|
|
if (activity == null) {
|
|
return
|
|
}
|
|
if (toBeScanned.isEmpty()) {
|
|
Toast.makeText(activity, R.string.nothing_to_scan, Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
MediaScannerConnection.scanFile(
|
|
requireContext(),
|
|
toBeScanned,
|
|
null,
|
|
UpdateToastMediaScannerCompletionListener(activity, listOf(*toBeScanned))
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun setCrumb(crumb: Crumb?, addToHistory: Boolean) {
|
|
if (crumb == null) {
|
|
return
|
|
}
|
|
val path = crumb.file.path
|
|
if (path == "/" || path == "/storage" || path == "/storage/emulated") {
|
|
switchToStorageAdapter()
|
|
} else {
|
|
saveScrollPosition()
|
|
binding.breadCrumbs.setActiveOrAdd(crumb, false)
|
|
if (addToHistory) {
|
|
binding.breadCrumbs.addHistory(crumb)
|
|
}
|
|
LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this)
|
|
}
|
|
}
|
|
|
|
private fun setUpAdapter() {
|
|
switchToFileAdapter()
|
|
}
|
|
|
|
private fun setUpBreadCrumbs() {
|
|
binding.breadCrumbs.setActivatedContentColor(
|
|
resolveColor(requireContext(), android.R.attr.textColorPrimary)
|
|
)
|
|
binding.breadCrumbs.setDeactivatedContentColor(
|
|
resolveColor(requireContext(), android.R.attr.textColorSecondary)
|
|
)
|
|
binding.breadCrumbs.setCallback(this)
|
|
}
|
|
|
|
private fun setUpRecyclerView() {
|
|
binding.recyclerView.layoutManager = LinearLayoutManager(
|
|
activity
|
|
)
|
|
val fastScroller = create(
|
|
binding.recyclerView
|
|
)
|
|
binding.recyclerView.setOnApplyWindowInsetsListener(
|
|
ScrollingViewOnApplyWindowInsetsListener(binding.recyclerView, fastScroller)
|
|
)
|
|
}
|
|
|
|
private fun toList(file: File): ArrayList<File> {
|
|
val files = ArrayList<File>(1)
|
|
files.add(file)
|
|
return files
|
|
}
|
|
|
|
private fun updateAdapter(files: List<File>) {
|
|
adapter?.swapDataSet(files)
|
|
val crumb = activeCrumb
|
|
if (crumb != null) {
|
|
(binding.recyclerView.layoutManager as LinearLayoutManager?)
|
|
?.scrollToPositionWithOffset(crumb.scrollPosition, 0)
|
|
}
|
|
}
|
|
|
|
override fun onDestroyView() {
|
|
super.onDestroyView()
|
|
_binding = null
|
|
}
|
|
|
|
class ListPathsAsyncTask(context: Context?, callback: OnPathsListedCallback) :
|
|
ListingFilesDialogAsyncTask<ListPathsAsyncTask.LoadingInfo, String?, Array<String?>>(
|
|
context
|
|
) {
|
|
private val onPathsListedCallbackWeakReference: WeakReference<OnPathsListedCallback> =
|
|
WeakReference(callback)
|
|
|
|
override fun doInBackground(vararg params: LoadingInfo): Array<String?> {
|
|
return try {
|
|
if (isCancelled || checkCallbackReference() == null) {
|
|
return arrayOf()
|
|
}
|
|
val info = params[0]
|
|
val paths: Array<String?>
|
|
if (info.file.isDirectory) {
|
|
val files = FileUtil.listFilesDeep(info.file, info.fileFilter)
|
|
if (isCancelled || checkCallbackReference() == null) {
|
|
return arrayOf()
|
|
}
|
|
paths = arrayOfNulls(files.size)
|
|
for (i in files.indices) {
|
|
val f = files[i]
|
|
paths[i] = FileUtil.safeGetCanonicalPath(f)
|
|
if (isCancelled || checkCallbackReference() == null) {
|
|
return arrayOf()
|
|
}
|
|
}
|
|
} else {
|
|
paths = arrayOfNulls(1)
|
|
paths[0] = info.file.path
|
|
}
|
|
paths
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
cancel(false)
|
|
arrayOf()
|
|
}
|
|
}
|
|
|
|
override fun onPostExecute(paths: Array<String?>) {
|
|
super.onPostExecute(paths)
|
|
checkCallbackReference()?.onPathsListed(paths)
|
|
}
|
|
|
|
override fun onPreExecute() {
|
|
super.onPreExecute()
|
|
checkCallbackReference()
|
|
}
|
|
|
|
private fun checkCallbackReference(): OnPathsListedCallback? {
|
|
val callback = onPathsListedCallbackWeakReference.get()
|
|
if (callback == null) {
|
|
cancel(false)
|
|
}
|
|
return callback
|
|
}
|
|
|
|
interface OnPathsListedCallback {
|
|
fun onPathsListed(paths: Array<String?>)
|
|
}
|
|
|
|
class LoadingInfo(val file: File, val fileFilter: FileFilter)
|
|
|
|
}
|
|
|
|
private class AsyncFileLoader(foldersFragment: FoldersFragment) :
|
|
WrappedAsyncTaskLoader<List<File>>(foldersFragment.requireActivity()) {
|
|
private val fragmentWeakReference: WeakReference<FoldersFragment> =
|
|
WeakReference(foldersFragment)
|
|
|
|
override fun loadInBackground(): List<File> {
|
|
val foldersFragment = fragmentWeakReference.get()
|
|
var directory: File? = null
|
|
if (foldersFragment != null) {
|
|
val crumb = foldersFragment.activeCrumb
|
|
if (crumb != null) {
|
|
directory = crumb.file
|
|
}
|
|
}
|
|
return if (directory != null) {
|
|
val files = FileUtil.listFiles(
|
|
directory,
|
|
AUDIO_FILE_FILTER
|
|
)
|
|
Collections.sort(files, foldersFragment!!.fileComparator)
|
|
files
|
|
} else {
|
|
LinkedList()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private open class ListSongsAsyncTask(
|
|
context: Context?,
|
|
private val extra: Any?,
|
|
callback: OnSongsListedCallback
|
|
) : ListingFilesDialogAsyncTask<ListSongsAsyncTask.LoadingInfo, Void, List<Song>>(context) {
|
|
private val callbackWeakReference = WeakReference(callback)
|
|
private val contextWeakReference = WeakReference(context)
|
|
override fun doInBackground(vararg params: LoadingInfo): List<Song> {
|
|
return try {
|
|
val info = params[0]
|
|
val files = FileUtil.listFilesDeep(info.files, info.fileFilter)
|
|
if (isCancelled || checkContextReference() == null || checkCallbackReference() == null) {
|
|
return emptyList()
|
|
}
|
|
Collections.sort(files, info.fileComparator)
|
|
val context = checkContextReference()
|
|
if (isCancelled || context == null || checkCallbackReference() == null) {
|
|
emptyList()
|
|
} else FileUtil.matchFilesWithMediaStore(context, files)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
cancel(false)
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
override fun onPostExecute(songs: List<Song>) {
|
|
super.onPostExecute(songs)
|
|
checkCallbackReference()?.onSongsListed(songs, extra)
|
|
}
|
|
|
|
override fun onPreExecute() {
|
|
super.onPreExecute()
|
|
checkCallbackReference()
|
|
checkContextReference()
|
|
}
|
|
|
|
private fun checkCallbackReference(): OnSongsListedCallback? {
|
|
val callback = callbackWeakReference.get()
|
|
if (callback == null) {
|
|
cancel(false)
|
|
}
|
|
return callback
|
|
}
|
|
|
|
private fun checkContextReference(): Context? {
|
|
val context = contextWeakReference.get()
|
|
if (context == null) {
|
|
cancel(false)
|
|
}
|
|
return context
|
|
}
|
|
|
|
interface OnSongsListedCallback {
|
|
fun onSongsListed(songs: List<Song>, extra: Any?)
|
|
}
|
|
|
|
class LoadingInfo(
|
|
val files: List<File>,
|
|
val fileFilter: FileFilter,
|
|
val fileComparator: Comparator<File>
|
|
)
|
|
|
|
}
|
|
|
|
abstract class ListingFilesDialogAsyncTask<Params, Progress, Result> :
|
|
DialogAsyncTask<Params, Progress, Result> {
|
|
internal constructor(context: Context?) : super(context)
|
|
|
|
override fun createDialog(context: Context): Dialog {
|
|
return MaterialAlertDialogBuilder(context)
|
|
.setTitle(R.string.listing_files)
|
|
.setCancelable(false)
|
|
.setView(R.layout.loading)
|
|
.setOnCancelListener { cancel(false) }
|
|
.setOnDismissListener { cancel(false) }
|
|
.create()
|
|
}
|
|
}
|
|
|
|
override fun onStorageClicked(storage: Storage) {
|
|
switchToFileAdapter()
|
|
setCrumb(
|
|
Crumb(
|
|
FileUtil.safeGetCanonicalFile(storage.file)
|
|
),
|
|
true
|
|
)
|
|
}
|
|
|
|
private fun switchToFileAdapter() {
|
|
adapter = SongFileAdapter(mainActivity, LinkedList(), R.layout.item_list, this, this)
|
|
adapter!!.registerAdapterDataObserver(
|
|
object : RecyclerView.AdapterDataObserver() {
|
|
override fun onChanged() {
|
|
super.onChanged()
|
|
checkIsEmpty()
|
|
checkForPadding()
|
|
}
|
|
})
|
|
binding.recyclerView.adapter = adapter
|
|
checkIsEmpty()
|
|
}
|
|
|
|
private fun switchToStorageAdapter() {
|
|
storageItems = FileUtil.listRoots()
|
|
storageAdapter = StorageAdapter(storageItems, this)
|
|
binding.recyclerView.adapter = storageAdapter
|
|
binding.breadCrumbs.clearCrumbs()
|
|
}
|
|
|
|
companion object {
|
|
val TAG: String = FoldersFragment::class.java.simpleName
|
|
val AUDIO_FILE_FILTER = FileFilter { file: File ->
|
|
(!file.isHidden
|
|
&& (file.isDirectory
|
|
|| FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton())
|
|
|| FileUtil.fileIsMimeType(file, "application/opus", MimeTypeMap.getSingleton())
|
|
|| FileUtil.fileIsMimeType(
|
|
file,
|
|
"application/ogg",
|
|
MimeTypeMap.getSingleton()
|
|
)))
|
|
}
|
|
private const val CRUMBS = "crumbs"
|
|
private const val LOADER_ID = 5
|
|
|
|
// root
|
|
val defaultStartDirectory: File
|
|
get() {
|
|
val musicDir =
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
|
|
val startFolder = if (musicDir.exists() && musicDir.isDirectory) {
|
|
musicDir
|
|
} else {
|
|
val externalStorage = Environment.getExternalStorageDirectory()
|
|
if (externalStorage.exists() && externalStorage.isDirectory) {
|
|
externalStorage
|
|
} else {
|
|
File("/") // root
|
|
}
|
|
}
|
|
return startFolder
|
|
}
|
|
|
|
private fun tryGetCanonicalFile(file: File): File {
|
|
return try {
|
|
file.canonicalFile
|
|
} catch (e: IOException) {
|
|
e.printStackTrace()
|
|
file
|
|
}
|
|
}
|
|
}
|
|
} |