diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/apps/manager/data/ApkFileManagerImpl.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/apps/manager/data/ApkFileManagerImpl.kt index b3a13551..e7c9b1ff 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/apps/manager/data/ApkFileManagerImpl.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/apps/manager/data/ApkFileManagerImpl.kt @@ -3,7 +3,6 @@ package com.d4rk.cleaner.app.apps.manager.data import android.app.Application import android.database.Cursor import android.net.Uri -import android.os.Environment import android.provider.MediaStore import com.d4rk.android.libs.apptoolkit.core.domain.model.network.DataState import com.d4rk.cleaner.app.apps.manager.domain.data.model.ApkInfo @@ -55,15 +54,18 @@ class ApkFileManagerImpl( } } - DirectoryScanner.scan( - root = Environment.getExternalStorageDirectory(), - skipDir = { dir -> dir.shouldSkip(showHidden) } - ) { file -> - if (file.shouldSkip(showHidden)) return@scan - if (file.extension.equals("apk", ignoreCase = true)) { - val path = file.absolutePath - if (file.exists() && file.canWrite() && addedPaths.add(path)) { - apkFiles.add(ApkInfo(file.hashCode().toLong(), path, file.length())) + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + if (root != null) { + DirectoryScanner.scan( + root = root, + skipDir = { dir -> dir.shouldSkip(showHidden) } + ) { file -> + if (file.shouldSkip(showHidden)) return@scan + if (file.extension.equals("apk", ignoreCase = true)) { + val path = file.absolutePath + if (file.exists() && file.canWrite() && addedPaths.add(path)) { + apkFiles.add(ApkInfo(file.hashCode().toLong(), path, file.length())) + } } } } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/data/MemoryRepositoryImpl.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/data/MemoryRepositoryImpl.kt index 56d850d4..b0dff6ce 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/data/MemoryRepositoryImpl.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/data/MemoryRepositoryImpl.kt @@ -52,7 +52,9 @@ class MemoryRepositoryImpl(private val application: Application) : MemoryReposit private fun getStorageBreakdown(context: Context): Map { val breakdown: MutableMap = mutableMapOf() - val externalStoragePath: String = Environment.getExternalStorageDirectory().absolutePath + val externalStoragePath: String = + context.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile?.absolutePath + ?: "/" breakdown[context.getString(R.string.installed_apps)] = getInstalledAppsSize(context) breakdown[context.getString(R.string.system)] = diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/ui/MemoryManagerScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/ui/MemoryManagerScreen.kt index c83f371e..600f191e 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/ui/MemoryManagerScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/memory/ui/MemoryManagerScreen.kt @@ -217,9 +217,14 @@ private fun handleStorageItemClick(context: Context, category: String) { } } - context.getString(R.string.other_files) -> FileManagerHelper.openFolderOrSettings( - context, - Environment.getExternalStorageDirectory() - ) + context.getString(R.string.other_files) -> { + val root = context.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + if (root != null) { + FileManagerHelper.openFolderOrSettings( + context, + root + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/data/ScannerRepositoryImpl.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/data/ScannerRepositoryImpl.kt index eceb5a11..9cdde806 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/data/ScannerRepositoryImpl.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/data/ScannerRepositoryImpl.kt @@ -89,10 +89,13 @@ class ScannerRepositoryImpl( } val allFoundExtensions: MutableSet = mutableSetOf() - DirectoryScanner.scan(Environment.getExternalStorageDirectory()) { file -> - val ext: String = file.extension.lowercase() - if (ext.isNotEmpty()) { - allFoundExtensions.add(ext) + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + if (root != null) { + DirectoryScanner.scan(root) { file -> + val ext: String = file.extension.lowercase() + if (ext.isNotEmpty()) { + allFoundExtensions.add(ext) + } } } @@ -117,7 +120,8 @@ class ScannerRepositoryImpl( override fun getAllFiles(onLockedDir: ((File) -> Unit)?): Flow = flow { val stack: ArrayDeque = ArrayDeque() - val root: File = Environment.getExternalStorageDirectory() + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + ?: return@flow stack.addFirst(root) val trashDir = @@ -271,7 +275,8 @@ class ScannerRepositoryImpl( override suspend fun getLargestFiles(limit: Int): List { return withContext(Dispatchers.IO) { - val root = Environment.getExternalStorageDirectory() + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + ?: return@withContext emptyList() val minSize = 100L * 1024 * 1024 // 100MB threshold val trashed = dataStore.trashFileOriginalPaths.first() val showHidden = dataStore.showHiddenFiles.first() @@ -345,7 +350,8 @@ class ScannerRepositoryImpl( override fun getEmptyFolders(): Flow = flow { val stack: ArrayDeque = ArrayDeque() - val root = Environment.getExternalStorageDirectory() + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile + ?: return@flow stack.addFirst(root) val showHidden = dataStore.showHiddenFiles.first() while (stack.isNotEmpty()) { diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/ui/ScannerViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/ui/ScannerViewModel.kt index 238e1a10..17dda113 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/ui/ScannerViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/ui/ScannerViewModel.kt @@ -719,7 +719,7 @@ class ScannerViewModel( private fun loadWhatsAppMedia() { _whatsAppMediaLoaded.value = false launch(context = dispatchers.io) { - val (images, videos, docs) = getWhatsAppMediaSummary() + val (images, videos, docs) = getWhatsAppMediaSummary(application) _whatsAppMediaSummary.value = WhatsAppMediaSummary( images = images, videos = videos, diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/StorageUtils.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/StorageUtils.kt index 47e75d8f..195958a4 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/StorageUtils.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/StorageUtils.kt @@ -3,7 +3,6 @@ package com.d4rk.cleaner.app.clean.scanner.utils.helpers import android.app.usage.StorageStatsManager import android.content.Context import android.os.Build -import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager import android.os.storage.StorageVolume @@ -30,7 +29,9 @@ object StorageUtils { totalSize = storageStatsManager.getTotalBytes(uuid) usedSize = totalSize - storageStatsManager.getFreeBytes(uuid) } else { - val statFs = StatFs(Environment.getExternalStorageDirectory().path) + val path = context.getExternalFilesDir(null)?.path + ?: context.filesDir.path + val statFs = StatFs(path) totalSize = statFs.blockSizeLong * statFs.blockCountLong usedSize = totalSize - (statFs.blockSizeLong * statFs.availableBlocksLong) } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/WhatsAppUtils.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/WhatsAppUtils.kt index f41fbf59..b9ba602b 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/WhatsAppUtils.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/scanner/utils/helpers/WhatsAppUtils.kt @@ -1,27 +1,44 @@ package com.d4rk.cleaner.app.clean.scanner.utils.helpers -import android.os.Environment +import android.content.Context import com.d4rk.cleaner.app.clean.whatsapp.utils.constants.WhatsAppMediaConstants import java.io.File -fun getWhatsAppMediaDirs(): File? { - val legacy = File(Environment.getExternalStorageDirectory(), "WhatsApp/Media") +/** + * Returns the base WhatsApp media directory or `null` if not found. + * The legacy `/WhatsApp/Media` path is checked first. If the directory + * does not exist, the scoped storage location under + * `/Android/media/com.whatsapp/WhatsApp/Media` is checked. + * + * When neither directory exists, `null` is returned, signalling that WhatsApp + * media is not accessible on the device. + */ +fun getWhatsAppMediaDirs(context: Context): File? { + val root = context.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile ?: return null + val legacy = File(root, "WhatsApp/Media") if (legacy.exists()) return legacy - val scoped = - File(Environment.getExternalStorageDirectory(), "Android/media/com.whatsapp/WhatsApp/Media") + val scoped = File(root, "Android/media/com.whatsapp/WhatsApp/Media") return scoped.takeIf { it.exists() } } -fun getWhatsAppMediaSummary(): Triple, List, List> { - val mediaDir = getWhatsAppMediaDirs() ?: return Triple(emptyList(), emptyList(), emptyList()) +/** + * Provides a summary of WhatsApp media files. If the WhatsApp media directories + * are absent, an empty triple is returned. + */ +fun getWhatsAppMediaSummary(context: Context): Triple, List, List> { + val mediaDir = getWhatsAppMediaDirs(context) + ?: return Triple(emptyList(), emptyList(), emptyList()) fun list(dirName: String): List { val dir = File(mediaDir, dirName) return dir.listFiles()?.filter { it.isFile && !it.name.startsWith(".") } ?.sortedByDescending { it.lastModified() } ?: emptyList() } - val images = list(WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.IMAGES]!!) - val videos = list(WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.VIDEOS]!!) - val docs = list(WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.DOCUMENTS]!!) + val images = + WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.IMAGES]?.let(::list) ?: emptyList() + val videos = + WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.VIDEOS]?.let(::list) ?: emptyList() + val docs = + WhatsAppMediaConstants.DIRECTORIES[WhatsAppMediaConstants.DOCUMENTS]?.let(::list) ?: emptyList() return Triple(images, videos, docs) } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/data/WhatsAppCleanerRepositoryImpl.kt b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/data/WhatsAppCleanerRepositoryImpl.kt index f5cba871..4c5e1a27 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/data/WhatsAppCleanerRepositoryImpl.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/data/WhatsAppCleanerRepositoryImpl.kt @@ -1,7 +1,6 @@ package com.d4rk.cleaner.app.clean.whatsapp.summary.data import android.app.Application -import android.os.Environment import android.text.format.Formatter import com.d4rk.cleaner.app.clean.whatsapp.summary.domain.model.DeleteResult import com.d4rk.cleaner.app.clean.whatsapp.summary.domain.model.DirectorySummary @@ -15,25 +14,24 @@ import java.io.File class WhatsAppCleanerRepositoryImpl(private val application: Application) : WhatsAppCleanerRepository { - - private fun getWhatsAppMediaDir(): File { - val scoped = File( - Environment.getExternalStorageDirectory(), - "Android/media/com.whatsapp/WhatsApp/Media" - ) - val legacy = File( - Environment.getExternalStorageDirectory(), - "WhatsApp/Media" - ) + /** + * Locate the base WhatsApp media directory. Returns `null` when neither the + * legacy nor the scoped-storage path exists, indicating that WhatsApp media + * is not available on the device. + */ + private fun getWhatsAppMediaDir(): File? { + val root = application.getExternalFilesDir(null)?.parentFile?.parentFile?.parentFile ?: return null + val scoped = File(root, "Android/media/com.whatsapp/WhatsApp/Media") + val legacy = File(root, "WhatsApp/Media") return when { scoped.exists() -> scoped legacy.exists() -> legacy - else -> scoped + else -> null } } override suspend fun getMediaSummary(): WhatsAppMediaSummary = withContext(Dispatchers.IO) { - val base = getWhatsAppMediaDir() + val base = getWhatsAppMediaDir() ?: return@withContext WhatsAppMediaSummary() fun collect(name: String): DirectorySummary { val dir = File(base, name) @@ -97,7 +95,7 @@ class WhatsAppCleanerRepositoryImpl(private val application: Application) : override suspend fun listMediaFiles(type: String, offset: Int, limit: Int): List = withContext(Dispatchers.IO) { - val base = getWhatsAppMediaDir() + val base = getWhatsAppMediaDir() ?: return@withContext emptyList() val dirName = WhatsAppMediaConstants.DIRECTORIES[type] ?: return@withContext emptyList() val dir = File(base, dirName) diff --git a/docs/cleaning_features.md b/docs/cleaning_features.md index a56e662d..c878dc76 100644 --- a/docs/cleaning_features.md +++ b/docs/cleaning_features.md @@ -9,6 +9,7 @@ All cleanup jobs enqueue a `FileCleanupWorker` via the centralized [FileCleanWor - ViewModel: [WhatsappCleanerSummaryViewModel](../app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/ui/WhatsappCleanerSummaryViewModel.kt) - Use cases: [GetWhatsAppMediaSummaryUseCase](../app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/domain/usecases/GetWhatsAppMediaSummaryUseCase.kt), [GetWhatsAppMediaFilesUseCase](../app/src/main/kotlin/com/d4rk/cleaner/app/clean/whatsapp/summary/domain/usecases/GetWhatsAppMediaFilesUseCase.kt) - Work ID key: `whatsappCleanWorkId` +- If no WhatsApp media directories are found, the feature returns empty results. ## Large Files Cleaner - Finds and deletes oversized files across storage.