diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b44944..cdb9f3d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ android { applicationId = "com.d4rk.cleaner" minSdk = 26 targetSdk = 34 - versionCode = 81 + versionCode = 84 versionName = "2.0.0" archivesName = "${applicationId}-v${versionName}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/ApkInfo.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/ApkInfo.kt new file mode 100644 index 0000000..bf75d35 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/ApkInfo.kt @@ -0,0 +1,7 @@ +package com.d4rk.cleaner.data.model.ui + +data class ApkInfo( + val id: Long, + val path: String, + val size: Long +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerComposable.kt index 7040eca..1fb6672 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerComposable.kt @@ -1,13 +1,12 @@ package com.d4rk.cleaner.ui.appmanager +import android.app.Application import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.net.Uri -import android.provider.MediaStore import android.provider.Settings import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box @@ -33,7 +32,7 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -49,24 +48,24 @@ import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.d4rk.cleaner.R +import com.d4rk.cleaner.data.model.ui.ApkInfo import java.io.File /** * Composable function for managing and displaying different app categories. - * - * This composable function displays tabs for "Installed Apps", "System Apps", and "App Install Files". - * Each tab shows corresponding app information based on the selected category. */ @Composable fun AppManagerComposable() { + + val viewModel: AppManagerViewModel = viewModel( + factory = AppManagerViewModelFactory(LocalContext.current.applicationContext as Application) + ) val tabs = listOf("Installed Apps", "System Apps", "App Install Files") var selectedIndex by remember { mutableIntStateOf(0) } - var apps by remember { mutableStateOf(listOf()) } - val context = LocalContext.current - LaunchedEffect(Unit) { - apps = context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) - } + val installedApps by viewModel.installedApps.collectAsState() + val apkFiles by viewModel.apkFiles.collectAsState() Column { TabRow( @@ -94,9 +93,11 @@ fun AppManagerComposable() { } } when (selectedIndex) { - 0 -> AppsComposable(apps.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 }) - 1 -> AppsComposable(apps.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 }) - 2 -> ApksComposable() + 0 -> AppsComposable( + apps = installedApps.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 }) + 1 -> AppsComposable( + apps = installedApps.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 }) + 2 -> ApksComposable(apkFiles = apkFiles) } } } @@ -181,7 +182,9 @@ fun AppItemComposable( } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(text = { Text(stringResource(R.string.uninstall)) }, + DropdownMenuItem(text = { + Text(stringResource(R.string.uninstall)) + }, onClick = { val uri = Uri.fromParts("package", app.packageName, null) val intent = Intent(Intent.ACTION_DELETE, uri) @@ -220,33 +223,16 @@ fun AppItemComposable( * Composable function for displaying a list of APK files on the device. */ @Composable -fun ApksComposable() { - val context = LocalContext.current - val uri = MediaStore.Files.getContentUri("external") - val cursor = context.contentResolver.query( - uri, - arrayOf(MediaStore.Files.FileColumns.DATA), - MediaStore.Files.FileColumns.MIME_TYPE + "=?", - arrayOf("application/vnd.android.package-archive"), - null - ) - - var apkPaths = listOf() - cursor?.use { - while (it.moveToNext()) { - val dataColumnIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATA) - val filePath = it.getString(dataColumnIndex) - apkPaths = apkPaths + filePath - } - } +fun ApksComposable(apkFiles: List) { LazyColumn { - items(apkPaths) { apkPath -> - ApkItemComposable(apkPath) + items(apkFiles) { apkInfo -> + ApkItemComposable(apkPath = apkInfo.path) } } } + /** * Composable function for displaying detailed information about an APK file. * @@ -321,7 +307,8 @@ fun ApkItemComposable(apkPath: String) { DropdownMenuItem(text = { Text("Install") }, onClick = { val installIntent = Intent(Intent.ACTION_VIEW) installIntent.setDataAndType( - Uri.fromFile(apkFile), "application/vnd.android.package-archive" + Uri.fromFile(apkFile), + "application/vnd.android.package-archive" ) installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(installIntent) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt new file mode 100644 index 0000000..a622b97 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt @@ -0,0 +1,80 @@ +package com.d4rk.cleaner.ui.appmanager + +import android.app.Application +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.database.Cursor +import android.net.Uri +import android.provider.MediaStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.d4rk.cleaner.data.model.ui.ApkInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AppManagerViewModel(private val application: Application) : ViewModel() { + private val _installedApps = MutableStateFlow>(emptyList()) + val installedApps: StateFlow> = _installedApps.asStateFlow() + + private val _apkFiles = MutableStateFlow>(emptyList()) + val apkFiles: StateFlow> = _apkFiles.asStateFlow() + + + init { + loadInstalledApps() + loadApkFiles() + } + + private fun loadInstalledApps() { + viewModelScope.launch { + _installedApps.value = getInstalledApps() + } + } + + private suspend fun getInstalledApps(): List { + return withContext(Dispatchers.IO) { + application.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) + } + } + + private fun loadApkFiles() { + viewModelScope.launch { + _apkFiles.value = getApkFilesFromStorage() + } + } + + private suspend fun getApkFilesFromStorage(): List { + return withContext(Dispatchers.IO) { + val apkFiles = mutableListOf() + val uri: Uri = MediaStore.Files.getContentUri("external") + val projection = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.SIZE + ) + val selection = "${MediaStore.Files.FileColumns.MIME_TYPE} = ?" + val selectionArgs = arrayOf("application/vnd.android.package-archive") + val cursor: Cursor? = application.contentResolver.query( + uri, projection, selection, selectionArgs, null + ) + + cursor?.use { + val idColumn = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val dataColumn = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) + + while (it.moveToNext()) { + val id = it.getLong(idColumn) + val path = it.getString(dataColumn) + val size = it.getLong(sizeColumn) + apkFiles.add(ApkInfo(id, path, size)) + } + } + apkFiles + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModelFactory.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModelFactory.kt new file mode 100644 index 0000000..d968c5d --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModelFactory.kt @@ -0,0 +1,15 @@ +package com.d4rk.cleaner.ui.appmanager + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class AppManagerViewModelFactory(private val application: Application) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AppManagerViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return AppManagerViewModel(application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file