diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/activities/MimeTypesActivity.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/activities/MimeTypesActivity.kt index 85942400..60c26b93 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/activities/MimeTypesActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/activities/MimeTypesActivity.kt @@ -40,6 +40,7 @@ class MimeTypesActivity : SimpleActivity(), ItemOperationsListener { private var zoomListener: MyRecyclerView.MyZoomListener? = null private var storedItems = ArrayList() private var currentViewType = VIEW_TYPE_LIST + private var currentVolume = PRIMARY_VOLUME_NAME override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true @@ -53,6 +54,7 @@ class MimeTypesActivity : SimpleActivity(), ItemOperationsListener { } currentMimeType = intent.getStringExtra(SHOW_MIMETYPE) ?: return + currentVolume = intent.getStringExtra(VOLUME_NAME) ?: currentVolume binding.mimetypesToolbar.title = getString( when (currentMimeType) { IMAGES -> R.string.images @@ -267,7 +269,7 @@ class MimeTypesActivity : SimpleActivity(), ItemOperationsListener { private fun getProperFileDirItems(callback: (ArrayList) -> Unit) { val fileDirItems = ArrayList() val showHidden = config.shouldShowHidden() - val uri = MediaStore.Files.getContentUri("external") + val uri = MediaStore.Files.getContentUri(currentVolume) val projection = arrayOf( MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.DATA, diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/extensions/Context.kt index 07b1f5f5..7c14deb9 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/extensions/Context.kt @@ -1,10 +1,29 @@ package com.simplemobiletools.filemanager.pro.extensions import android.content.Context +import android.os.storage.StorageManager import com.simplemobiletools.commons.extensions.isPathOnOTG import com.simplemobiletools.commons.extensions.isPathOnSD +import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.filemanager.pro.helpers.Config +import com.simplemobiletools.filemanager.pro.helpers.PRIMARY_VOLUME_NAME +import java.util.Locale val Context.config: Config get() = Config.newInstance(applicationContext) fun Context.isPathOnRoot(path: String) = !(path.startsWith(config.internalStoragePath) || isPathOnOTG(path) || (isPathOnSD(path))) + +fun Context.getAllVolumeNames(): List { + val volumeNames = mutableListOf(PRIMARY_VOLUME_NAME) + if (isNougatPlus()) { + val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager + getExternalFilesDirs(null) + .mapNotNull { storageManager.getStorageVolume(it) } + .filterNot { it.isPrimary } + .mapNotNull { it.uuid?.lowercase(Locale.US) } + .forEach { + volumeNames.add(it) + } + } + return volumeNames +} diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/fragments/StorageFragment.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/fragments/StorageFragment.kt index 6db802c8..bd33401b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/fragments/StorageFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/fragments/StorageFragment.kt @@ -6,13 +6,17 @@ import android.app.usage.StorageStatsManager import android.content.ContentResolver import android.content.Context import android.content.Intent +import android.media.MediaScannerConnection import android.os.Handler +import android.os.Looper import android.os.storage.StorageManager import android.provider.MediaStore import android.provider.Settings import android.util.AttributeSet import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf +import androidx.core.view.children +import androidx.core.view.isVisible import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.FileDirItem @@ -21,12 +25,15 @@ import com.simplemobiletools.filemanager.pro.R import com.simplemobiletools.filemanager.pro.activities.MimeTypesActivity import com.simplemobiletools.filemanager.pro.activities.SimpleActivity import com.simplemobiletools.filemanager.pro.adapters.ItemsAdapter +import com.simplemobiletools.filemanager.pro.databinding.ItemStorageVolumeBinding import com.simplemobiletools.filemanager.pro.databinding.StorageFragmentBinding import com.simplemobiletools.filemanager.pro.extensions.config import com.simplemobiletools.filemanager.pro.extensions.formatSizeThousand +import com.simplemobiletools.filemanager.pro.extensions.getAllVolumeNames import com.simplemobiletools.filemanager.pro.helpers.* import com.simplemobiletools.filemanager.pro.interfaces.ItemOperationsListener import com.simplemobiletools.filemanager.pro.models.ListItem +import java.io.File import java.util.* class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), @@ -35,6 +42,7 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage private var allDeviceListItems = ArrayList() private var lastSearchedText = "" private lateinit var binding: StorageFragmentBinding + private val volumes = mutableMapOf() override fun onFinishInflate() { super.onFinishInflate() @@ -47,88 +55,132 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage this.activity = activity } - binding.totalSpace.text = String.format(context.getString(R.string.total_storage), "…") - getSizes() + val volumeNames = activity.getAllVolumeNames() + volumeNames.forEach { volumeName -> + val volumeBinding = ItemStorageVolumeBinding.inflate(activity.layoutInflater) + volumes[volumeName] = volumeBinding + volumeBinding.apply { + if (volumeName == PRIMARY_VOLUME_NAME) { + storageName.setText(R.string.internal) + } else { + storageName.setText(R.string.sd_card) + } - binding.freeSpaceHolder.setOnClickListener { - try { - val storageSettingsIntent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS) - activity.startActivity(storageSettingsIntent) - } catch (e: Exception) { - activity.showErrorToast(e) + totalSpace.text = String.format(context.getString(R.string.total_storage), "…") + getSizes(volumeName) + + if (volumeNames.size > 1) { + root.children.forEach { it.beGone() } + freeSpaceHolder.beVisible() + expandButton.applyColorFilter(context.getProperPrimaryColor()) + expandButton.setImageResource(R.drawable.ic_arrow_down_vector) + + expandButton.setOnClickListener { _ -> + if (imagesHolder.isVisible) { + root.children.filterNot { it == freeSpaceHolder }.forEach { it.beGone() } + expandButton.setImageResource(R.drawable.ic_arrow_down_vector) + } else { + root.children.filterNot { it == freeSpaceHolder }.forEach { it.beVisible() } + expandButton.setImageResource(R.drawable.ic_arrow_up_vector) + } + } + } else { + expandButton.beGone() + } + + freeSpaceHolder.setOnClickListener { + try { + val storageSettingsIntent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS) + activity.startActivity(storageSettingsIntent) + } catch (e: Exception) { + activity.showErrorToast(e) + } + } + + imagesHolder.setOnClickListener { launchMimetypeActivity(IMAGES, volumeName) } + videosHolder.setOnClickListener { launchMimetypeActivity(VIDEOS, volumeName) } + audioHolder.setOnClickListener { launchMimetypeActivity(AUDIO, volumeName) } + documentsHolder.setOnClickListener { launchMimetypeActivity(DOCUMENTS, volumeName) } + archivesHolder.setOnClickListener { launchMimetypeActivity(ARCHIVES, volumeName) } + othersHolder.setOnClickListener { launchMimetypeActivity(OTHERS, volumeName) } } + binding.storageVolumesHolder.addView(volumeBinding.root) } - binding.apply { - imagesHolder.setOnClickListener { launchMimetypeActivity(IMAGES) } - videosHolder.setOnClickListener { launchMimetypeActivity(VIDEOS) } - audioHolder.setOnClickListener { launchMimetypeActivity(AUDIO) } - documentsHolder.setOnClickListener { launchMimetypeActivity(DOCUMENTS) } - archivesHolder.setOnClickListener { launchMimetypeActivity(ARCHIVES) } - othersHolder.setOnClickListener { launchMimetypeActivity(OTHERS) } + ensureBackgroundThread { + getVolumeStorageStats(context) } - Handler().postDelayed({ + Handler(Looper.getMainLooper()).postDelayed({ refreshFragment() }, 2000) } override fun onResume(textColor: Int) { - getSizes() context.updateTextColors(binding.root) + val properPrimaryColor = context.getProperPrimaryColor() + val redColor = context.resources.getColor(R.color.md_red_700) + val greenColor = context.resources.getColor(R.color.md_green_700) + val lightBlueColor = context.resources.getColor(R.color.md_light_blue_700) + val yellowColor = context.resources.getColor(R.color.md_yellow_700) + val tealColor = context.resources.getColor(R.color.md_teal_700) + val pinkColor = context.resources.getColor(R.color.md_pink_700) + + volumes.entries.forEach { (it, volumeBinding) -> + getSizes(it) + volumeBinding.apply { + mainStorageUsageProgressbar.setIndicatorColor(properPrimaryColor) + mainStorageUsageProgressbar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) + + imagesProgressbar.setIndicatorColor(redColor) + imagesProgressbar.trackColor = redColor.adjustAlpha(LOWER_ALPHA) + + videosProgressbar.setIndicatorColor(greenColor) + videosProgressbar.trackColor = greenColor.adjustAlpha(LOWER_ALPHA) + + audioProgressbar.setIndicatorColor(lightBlueColor) + audioProgressbar.trackColor = lightBlueColor.adjustAlpha(LOWER_ALPHA) + + documentsProgressbar.setIndicatorColor(yellowColor) + documentsProgressbar.trackColor = yellowColor.adjustAlpha(LOWER_ALPHA) + + archivesProgressbar.setIndicatorColor(tealColor) + archivesProgressbar.trackColor = tealColor.adjustAlpha(LOWER_ALPHA) + + othersProgressbar.setIndicatorColor(pinkColor) + othersProgressbar.trackColor = pinkColor.adjustAlpha(LOWER_ALPHA) + + expandButton.applyColorFilter(context.getProperPrimaryColor()) + } + } + binding.apply { - val properPrimaryColor = context.getProperPrimaryColor() - mainStorageUsageProgressbar.setIndicatorColor(properPrimaryColor) - mainStorageUsageProgressbar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) - - val redColor = context.resources.getColor(R.color.md_red_700) - imagesProgressbar.setIndicatorColor(redColor) - imagesProgressbar.trackColor = redColor.adjustAlpha(LOWER_ALPHA) - - val greenColor = context.resources.getColor(R.color.md_green_700) - videosProgressbar.setIndicatorColor(greenColor) - videosProgressbar.trackColor = greenColor.adjustAlpha(LOWER_ALPHA) - - val lightBlueColor = context.resources.getColor(R.color.md_light_blue_700) - audioProgressbar.setIndicatorColor(lightBlueColor) - audioProgressbar.trackColor = lightBlueColor.adjustAlpha(LOWER_ALPHA) - - val yellowColor = context.resources.getColor(R.color.md_yellow_700) - documentsProgressbar.setIndicatorColor(yellowColor) - documentsProgressbar.trackColor = yellowColor.adjustAlpha(LOWER_ALPHA) - - val tealColor = context.resources.getColor(R.color.md_teal_700) - archivesProgressbar.setIndicatorColor(tealColor) - archivesProgressbar.trackColor = tealColor.adjustAlpha(LOWER_ALPHA) - - val pinkColor = context.resources.getColor(R.color.md_pink_700) - othersProgressbar.setIndicatorColor(pinkColor) - othersProgressbar.trackColor = pinkColor.adjustAlpha(LOWER_ALPHA) - searchHolder.setBackgroundColor(context.getProperBackgroundColor()) progressBar.setIndicatorColor(properPrimaryColor) progressBar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) } + ensureBackgroundThread { + getVolumeStorageStats(context) + } } - private fun launchMimetypeActivity(mimetype: String) { + private fun launchMimetypeActivity(mimetype: String, volumeName: String) { Intent(context, MimeTypesActivity::class.java).apply { putExtra(SHOW_MIMETYPE, mimetype) + putExtra(VOLUME_NAME, volumeName) context.startActivity(this) } } - private fun getSizes() { + private fun getSizes(volumeName: String) { if (!isOreoPlus()) { return } ensureBackgroundThread { - getMainStorageStats(context) - - val filesSize = getSizesByMimeType() + val filesSize = getSizesByMimeType(volumeName) val fileSizeImages = filesSize[IMAGES]!! val fileSizeVideos = filesSize[VIDEOS]!! val fileSizeAudios = filesSize[AUDIO]!! @@ -137,7 +189,7 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage val fileSizeOthers = filesSize[OTHERS]!! post { - binding.apply { + volumes[volumeName]!!.apply { imagesSize.text = fileSizeImages.formatSize() imagesProgressbar.progress = (fileSizeImages / SIZE_DIVIDER).toInt() @@ -160,8 +212,8 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage } } - private fun getSizesByMimeType(): HashMap { - val uri = MediaStore.Files.getContentUri("external") + private fun getSizesByMimeType(volumeName: String): HashMap { + val uri = MediaStore.Files.getContentUri(volumeName) val projection = arrayOf( MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.MIME_TYPE, @@ -222,40 +274,70 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage } @SuppressLint("NewApi") - private fun getMainStorageStats(context: Context) { + private fun getVolumeStorageStats(context: Context) { val externalDirs = context.getExternalFilesDirs(null) val storageManager = context.getSystemService(AppCompatActivity.STORAGE_SERVICE) as StorageManager externalDirs.forEach { file -> + val volumeName: String + val totalStorageSpace: Long + val freeStorageSpace: Long val storageVolume = storageManager.getStorageVolume(file) ?: return if (storageVolume.isPrimary) { // internal storage - val storageStatsManager = context.getSystemService(AppCompatActivity.STORAGE_STATS_SERVICE) as StorageStatsManager - val uuid = StorageManager.UUID_DEFAULT - val totalStorageSpace = storageStatsManager.getTotalBytes(uuid) - val freeStorageSpace = storageStatsManager.getFreeBytes(uuid) - - post { - binding.apply { - arrayOf( - mainStorageUsageProgressbar, imagesProgressbar, videosProgressbar, audioProgressbar, documentsProgressbar, - archivesProgressbar, othersProgressbar - ).forEach { - it.max = (totalStorageSpace / SIZE_DIVIDER).toInt() - } - - mainStorageUsageProgressbar.progress = ((totalStorageSpace - freeStorageSpace) / SIZE_DIVIDER).toInt() - - mainStorageUsageProgressbar.beVisible() - freeSpaceValue.text = freeStorageSpace.formatSizeThousand() - totalSpace.text = String.format(context.getString(R.string.total_storage), totalStorageSpace.formatSizeThousand()) - freeSpaceLabel.beVisible() - } + volumeName = PRIMARY_VOLUME_NAME + if (isOreoPlus()) { + val storageStatsManager = context.getSystemService(AppCompatActivity.STORAGE_STATS_SERVICE) as StorageStatsManager + val uuid = StorageManager.UUID_DEFAULT + totalStorageSpace = storageStatsManager.getTotalBytes(uuid) + freeStorageSpace = storageStatsManager.getFreeBytes(uuid) + } else { + totalStorageSpace = file.totalSpace + freeStorageSpace = file.freeSpace } } else { - // sd card - val totalSpace = file.totalSpace - val freeSpace = file.freeSpace + volumeName = storageVolume.uuid!!.lowercase(Locale.US) + totalStorageSpace = file.totalSpace + freeStorageSpace = file.freeSpace + post { + ensureBackgroundThread { + scanVolume(volumeName, file) + } + } + } + + post { + volumes[volumeName]?.apply { + arrayOf( + mainStorageUsageProgressbar, imagesProgressbar, videosProgressbar, audioProgressbar, documentsProgressbar, + archivesProgressbar, othersProgressbar + ).forEach { + it.max = (totalStorageSpace / SIZE_DIVIDER).toInt() + } + + mainStorageUsageProgressbar.progress = ((totalStorageSpace - freeStorageSpace) / SIZE_DIVIDER).toInt() + + mainStorageUsageProgressbar.beVisible() + freeSpaceValue.text = freeStorageSpace.formatSizeThousand() + totalSpace.text = String.format(context.getString(R.string.total_storage), totalStorageSpace.formatSizeThousand()) + freeSpaceLabel.beVisible() + } + } + } + } + + private fun scanVolume(volumeName: String, root: File) { + val paths = mutableListOf() + if (context.isPathOnSD(root.path)) { + File(context.sdCardPath).walkBottomUp().forEach { + paths.add(it.path) + } + } + var callbackCount = 0 + MediaScannerConnection.scanFile(context, paths.toTypedArray(), null) { _, _ -> + callbackCount++ + if (callbackCount == paths.size) { + getSizes(volumeName) } } } @@ -334,10 +416,10 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage } } - private fun getAllFiles(): ArrayList { + private fun getAllFiles(volumeName: String): ArrayList { val fileDirItems = ArrayList() val showHidden = context?.config?.shouldShowHidden() ?: return fileDirItems - val uri = MediaStore.Files.getContentUri("external") + val uri = MediaStore.Files.getContentUri(volumeName) val projection = arrayOf( MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.DISPLAY_NAME, @@ -396,8 +478,8 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage override fun refreshFragment() { ensureBackgroundThread { - val fileDirItems = getAllFiles() - allDeviceListItems = getListItemsFromFileDirItems(fileDirItems) + val fileDirItems = volumes.keys.map { getAllFiles(it) }.flatten() + allDeviceListItems = getListItemsFromFileDirItems(ArrayList(fileDirItems)) } setupLayoutManager() } diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/Constants.kt index ff9cab7e..28e1ae40 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/Constants.kt @@ -41,6 +41,9 @@ const val ARCHIVES = "archives" const val OTHERS = "others" const val SHOW_MIMETYPE = "show_mimetype" +const val VOLUME_NAME = "volume_name" +const val PRIMARY_VOLUME_NAME = "external_primary" + // what else should we count as an audio except "audio/*" mimetype val extraAudioMimeTypes = arrayListOf("application/ogg") val extraDocumentMimeTypes = arrayListOf( diff --git a/app/src/main/res/drawable/ic_arrow_down_vector.xml b/app/src/main/res/drawable/ic_arrow_down_vector.xml new file mode 100644 index 00000000..1aeaa998 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_vector.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_vector.xml b/app/src/main/res/drawable/ic_arrow_up_vector.xml new file mode 100644 index 00000000..1d112693 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_vector.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/item_storage_volume.xml b/app/src/main/res/layout/item_storage_volume.xml new file mode 100644 index 00000000..2f84d9cf --- /dev/null +++ b/app/src/main/res/layout/item_storage_volume.xml @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/storage_fragment.xml b/app/src/main/res/layout/storage_fragment.xml index 00ca6c97..7a52a657 100644 --- a/app/src/main/res/layout/storage_fragment.xml +++ b/app/src/main/res/layout/storage_fragment.xml @@ -11,309 +11,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:orientation="vertical" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -