diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 22a7518..c7c7788 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 = 95 + versionCode = 96 versionName = "2.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf( 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 bb685c1..3c82c02 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 @@ -31,6 +31,7 @@ import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -40,6 +41,8 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -54,6 +57,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringResource @@ -69,89 +73,97 @@ import java.io.File /** * Composable function for managing and displaying different app categories. */ -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class , ExperimentalMaterial3Api::class) @Composable fun AppManagerComposable() { - val viewModel: AppManagerViewModel = viewModel( + val viewModel : AppManagerViewModel = viewModel( factory = AppManagerViewModelFactory(LocalContext.current.applicationContext as Application) ) val context = LocalContext.current val tabs = listOf( - stringResource(id = R.string.installed_apps), - stringResource(id = R.string.system_apps), - stringResource(id = R.string.app_install_files) + stringResource(id = R.string.installed_apps) , + stringResource(id = R.string.system_apps) , + stringResource(id = R.string.app_install_files) , ) val pagerState = rememberPagerState(pageCount = { tabs.size }) val coroutineScope = rememberCoroutineScope() val isLoading by viewModel.isLoading.collectAsState() - val transition = updateTransition(targetState = ! isLoading, label = "LoadingTransition") + val state = rememberPullToRefreshState() + val transition = updateTransition(targetState = ! isLoading , label = "LoadingTransition") val contentAlpha by transition.animateFloat(label = "Content Alpha") { if (it) 1f else 0f } LaunchedEffect(context) { - if (!PermissionsUtils.hasStoragePermissions(context)) { + if (! PermissionsUtils.hasStoragePermissions(context)) { PermissionsUtils.requestStoragePermissions(context as Activity) } } - if (isLoading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() + if (state.isRefreshing) { + LaunchedEffect(true) { + viewModel.loadAppData() + state.endRefresh() } - } else { - Column( - modifier = Modifier.alpha(contentAlpha), - ) { - TabRow( - selectedTabIndex = pagerState.currentPage, - indicator = { tabPositions -> - TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), - shape = RoundedCornerShape( - topStart = 3.dp, topEnd = 3.dp, bottomEnd = 0.dp, bottomStart = 0.dp - ), - ) - }, + } + + Box(Modifier.nestedScroll(state.nestedScrollConnection)) { + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center ) { - tabs.forEachIndexed { index, title -> - Tab( - text = { + CircularProgressIndicator() + } + } + else { + Column( + modifier = Modifier.alpha(contentAlpha) , + ) { + TabRow( + selectedTabIndex = pagerState.currentPage , + indicator = { tabPositions -> + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) , + shape = RoundedCornerShape( + topStart = 3.dp , + topEnd = 3.dp , + bottomEnd = 0.dp , + bottomStart = 0.dp , + ) , + ) + } , + ) { + tabs.forEachIndexed { index , title -> + Tab(text = { Text( - text = title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = title , + maxLines = 1 , + overflow = TextOverflow.Ellipsis , color = MaterialTheme.colorScheme.onSurface ) - }, - selected = pagerState.currentPage == index, - onClick = { + } , selected = pagerState.currentPage == index , onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } - } - ) + }) + } } - } - HorizontalPager( - state = pagerState, - ) { page -> - when (page) { - 0 -> AppsComposable( - apps = viewModel.installedApps.collectAsState().value.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 } - ) - 1 -> AppsComposable( - apps = viewModel.installedApps.collectAsState().value.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 } - ) - 2 -> ApksComposable(apkFiles = viewModel.apkFiles.collectAsState().value) + HorizontalPager( + state = pagerState , + ) { page -> + when (page) { + 0 -> AppsComposable(apps = viewModel.installedApps.collectAsState().value.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 }) + + 1 -> AppsComposable(apps = viewModel.installedApps.collectAsState().value.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 }) + + 2 -> ApksComposable(apkFiles = viewModel.apkFiles.collectAsState().value) + } } } } + PullToRefreshContainer(state = state , modifier = Modifier.align(Alignment.TopCenter)) } } @@ -161,7 +173,7 @@ fun AppManagerComposable() { * @param apps List of ApplicationInfo objects representing the apps to display. */ @Composable -fun AppsComposable(apps: List) { +fun AppsComposable(apps : List) { LazyColumn { items(apps) { app -> AppItemComposable(app) @@ -176,7 +188,7 @@ fun AppsComposable(apps: List) { */ @Composable fun AppItemComposable( - app: ApplicationInfo + app : ApplicationInfo ) { val context = LocalContext.current val packageManager = context.packageManager @@ -191,81 +203,85 @@ fun AppItemComposable( val drawable = app.loadIcon(packageManager) val bitmap = if (drawable is BitmapDrawable) { drawable.bitmap - } else { + } + else { val bitmap = Bitmap.createBitmap( - drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 + drawable.intrinsicWidth , drawable.intrinsicHeight , Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.setBounds(0 , 0 , canvas.width , canvas.height) drawable.draw(canvas) bitmap } bitmap.asImageBitmap() } var showMenu by remember { mutableStateOf(false) } - OutlinedCard(modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp)) { + OutlinedCard(modifier = Modifier.padding(start = 8.dp , end = 8.dp , top = 8.dp)) { Row( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(16.dp)), + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(16.dp)) , verticalAlignment = Alignment.CenterVertically ) { Image( - bitmap = appIcon, contentDescription = null, modifier = Modifier.size(48.dp) + bitmap = appIcon , contentDescription = null , modifier = Modifier.size(48.dp) ) Column( modifier = Modifier - .padding(16.dp) - .weight(1f) + .padding(16.dp) + .weight(1f) ) { Text( - text = appName, - style = MaterialTheme.typography.titleMedium, + text = appName , + style = MaterialTheme.typography.titleMedium , ) Text( - text = appSize, style = MaterialTheme.typography.bodyMedium + text = appSize , style = MaterialTheme.typography.bodyMedium ) } Box { IconButton(onClick = { showMenu = true }) { - Icon(Icons.Outlined.MoreVert, contentDescription = null) + Icon(Icons.Outlined.MoreVert , contentDescription = null) } - DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { + DropdownMenu(expanded = showMenu , onDismissRequest = { showMenu = false }) { DropdownMenuItem(text = { Text(stringResource(R.string.uninstall)) - }, - onClick = { - val uri = Uri.fromParts("package", app.packageName, null) - val intent = Intent(Intent.ACTION_DELETE, uri) - context.startActivity(intent) - }) - DropdownMenuItem(text = { Text(stringResource(R.string.share)) }, onClick = { + } , onClick = { + val uri = Uri.fromParts("package" , app.packageName , null) + val intent = Intent(Intent.ACTION_DELETE , uri) + context.startActivity(intent) + }) + DropdownMenuItem(text = { Text(stringResource(R.string.share)) } , onClick = { val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "text/plain" - shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Check out this app") + shareIntent.putExtra(Intent.EXTRA_SUBJECT , "Check out this app") @Suppress("DEPRECATION") val isFromPlayStore = - context.packageManager.getInstallerPackageName(app.packageName) == "com.android.vending" + context.packageManager.getInstallerPackageName(app.packageName) == "com.android.vending" if (isFromPlayStore) { val playStoreLink = - "https://play.google.com/store/apps/details?id=${app.packageName}" + "https://play.google.com/store/apps/details?id=${app.packageName}" val shareMessage = "Check out this app: $appName\n$playStoreLink" - shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) - } else { + shareIntent.putExtra(Intent.EXTRA_TEXT , shareMessage) + } + else { val shareMessage = "Check out this app: $appName\n$app.packageName" - shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) + shareIntent.putExtra(Intent.EXTRA_TEXT , shareMessage) } - context.startActivity(Intent.createChooser(shareIntent, "Share App")) - }) - DropdownMenuItem(text = { Text(stringResource(R.string.app_info)) }, onClick = { - val appInfoIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - val packageUri = Uri.fromParts("package", app.packageName, null) - appInfoIntent.data = packageUri - context.startActivity(appInfoIntent) + context.startActivity(Intent.createChooser(shareIntent , "Share App")) }) + DropdownMenuItem(text = { Text(stringResource(R.string.app_info)) } , + onClick = { + val appInfoIntent = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val packageUri = + Uri.fromParts("package" , app.packageName , null) + appInfoIntent.data = packageUri + context.startActivity(appInfoIntent) + }) } } } @@ -276,7 +292,7 @@ fun AppItemComposable( * Composable function for displaying a list of APK files on the device. */ @Composable -fun ApksComposable(apkFiles: List) { +fun ApksComposable(apkFiles : List) { LazyColumn { items(apkFiles) { apkInfo -> @@ -292,7 +308,7 @@ fun ApksComposable(apkFiles: List) { * @param apkPath Path to the APK file. */ @Composable -fun ApkItemComposable(apkPath: String) { +fun ApkItemComposable(apkPath : String) { val context = LocalContext.current val apkFile = File(apkPath) val sizeInBytes = apkFile.length() @@ -301,16 +317,17 @@ fun ApkItemComposable(apkPath: String) { val apkSize = "%.2f MB".format(sizeInMB.toFloat()) val apkName = apkFile.name - val packageInfo = context.packageManager.getPackageArchiveInfo(apkPath, 0) + val packageInfo = context.packageManager.getPackageArchiveInfo(apkPath , 0) val appIcon = packageInfo?.applicationInfo?.loadIcon(context.packageManager)?.let { val bitmap = if (it is BitmapDrawable) { it.bitmap - } else { + } + else { val bitmap = Bitmap.createBitmap( - it.intrinsicWidth, it.intrinsicHeight, Bitmap.Config.ARGB_8888 + it.intrinsicWidth , it.intrinsicHeight , Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) - it.setBounds(0, 0, canvas.width, canvas.height) + it.setBounds(0 , 0 , canvas.width , canvas.height) it.draw(canvas) bitmap } @@ -319,53 +336,54 @@ fun ApkItemComposable(apkPath: String) { var showMenu by remember { mutableStateOf(false) } - OutlinedCard(modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp)) { + OutlinedCard(modifier = Modifier.padding(start = 8.dp , end = 8.dp , top = 8.dp)) { Row( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(16.dp)), + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(16.dp)) , verticalAlignment = Alignment.CenterVertically ) { Image( - bitmap = appIcon, contentDescription = null, modifier = Modifier.size(48.dp) + bitmap = appIcon , contentDescription = null , modifier = Modifier.size(48.dp) ) Column( modifier = Modifier - .padding(16.dp) - .weight(1f) + .padding(16.dp) + .weight(1f) ) { Text( - text = apkName, - style = MaterialTheme.typography.titleMedium, + text = apkName , + style = MaterialTheme.typography.titleMedium , ) Text( - text = apkSize, style = MaterialTheme.typography.bodyMedium + text = apkSize , style = MaterialTheme.typography.bodyMedium ) } Box { IconButton(onClick = { showMenu = true }) { - Icon(Icons.Outlined.MoreVert, contentDescription = null) + Icon(Icons.Outlined.MoreVert , contentDescription = null) } - DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(text = { Text(stringResource(R.string.share)) }, onClick = { + DropdownMenu(expanded = showMenu , onDismissRequest = { showMenu = false }) { + DropdownMenuItem(text = { Text(stringResource(R.string.share)) } , onClick = { val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "application/vnd.android.package-archive" - shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(apkFile)) - context.startActivity(Intent.createChooser(shareIntent, "Share APK")) + shareIntent.putExtra(Intent.EXTRA_STREAM , Uri.fromFile(apkFile)) + context.startActivity(Intent.createChooser(shareIntent , "Share APK")) }) - DropdownMenuItem(text = { Text(stringResource(id = R.string.installed)) }, onClick = { - val installIntent = Intent(Intent.ACTION_VIEW) - installIntent.setDataAndType( - Uri.fromFile(apkFile), - "application/vnd.android.package-archive" - ) - installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(installIntent) - }) + DropdownMenuItem(text = { Text(stringResource(id = R.string.installed)) } , + onClick = { + val installIntent = Intent(Intent.ACTION_VIEW) + installIntent.setDataAndType( + 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 index 67daf01..71c3239 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt @@ -18,12 +18,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class AppManagerViewModel(private val application: Application) : ViewModel() { +class AppManagerViewModel(private val application : Application) : ViewModel() { private val _installedApps = MutableStateFlow>(emptyList()) - val installedApps: StateFlow> = _installedApps.asStateFlow() + val installedApps : StateFlow> = _installedApps.asStateFlow() private val _apkFiles = MutableStateFlow>(emptyList()) - val apkFiles: StateFlow> = _apkFiles.asStateFlow() + val apkFiles : StateFlow> = _apkFiles.asStateFlow() private val _isLoading = MutableStateFlow(true) val isLoading : StateFlow = _isLoading.asStateFlow() @@ -32,14 +32,13 @@ class AppManagerViewModel(private val application: Application) : ViewModel() { loadAppData() } - private fun loadAppData() { + fun loadAppData() { viewModelScope.launch { _isLoading.value = true try { - awaitAll( - async { loadInstalledApps() }, - async { loadApkFiles() } - ) + _installedApps.value = emptyList() + _apkFiles.value = emptyList() + awaitAll(async { loadInstalledApps() } , async { loadApkFiles() }) } finally { _isLoading.value = false } @@ -52,7 +51,7 @@ class AppManagerViewModel(private val application: Application) : ViewModel() { } } - private suspend fun getInstalledApps(): List { + private suspend fun getInstalledApps() : List { return withContext(Dispatchers.IO) { application.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) } @@ -64,19 +63,19 @@ class AppManagerViewModel(private val application: Application) : ViewModel() { } } - private suspend fun getApkFilesFromStorage(): List { + private suspend fun getApkFilesFromStorage() : List { return withContext(Dispatchers.IO) { val apkFiles = mutableListOf() - val uri: Uri = MediaStore.Files.getContentUri("external") + val uri : Uri = MediaStore.Files.getContentUri("external") val projection = arrayOf( - MediaStore.Files.FileColumns._ID, - MediaStore.Files.FileColumns.DATA, + 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 + val cursor : Cursor? = application.contentResolver.query( + uri , projection , selection , selectionArgs , null ) cursor?.use { @@ -88,7 +87,7 @@ class AppManagerViewModel(private val application: Application) : ViewModel() { val id = it.getLong(idColumn) val path = it.getString(dataColumn) val size = it.getLong(sizeColumn) - apkFiles.add(ApkInfo(id, path, size)) + apkFiles.add(ApkInfo(id , path , size)) } } apkFiles diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt index 5d0d98f..30bc28d 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt @@ -315,7 +315,6 @@ fun AnalyzeComposable(launchScanningKey: MutableState, imageLoader: Ima } } - @Composable fun FileCard(file: File, viewModel: HomeViewModel, imageLoader: ImageLoader) { val context = LocalContext.current