Skip to content

Commit f8989c4

Browse files
committed
prepare space management
1 parent 10afd9e commit f8989c4

11 files changed

Lines changed: 474 additions & 1 deletion

File tree

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
android:allowAudioPlaybackCapture="false"
3232
android:enableOnBackInvokedCallback="true"
3333
android:name=".App"
34+
android:manageSpaceActivity=".SpaceActivity"
3435
android:resizeableActivity="true"
3536
android:networkSecurityConfig="@xml/network_security"
3637
android:largeHeap="true"
@@ -77,6 +78,15 @@
7778
</intent-filter>
7879
</activity>
7980

81+
<activity
82+
android:name=".SpaceActivity"
83+
android:launchMode="singleInstance"
84+
android:configChanges="orientation|smallestScreenSize|screenSize|screenLayout|keyboardHidden"
85+
android:resizeableActivity="true"
86+
android:theme="@style/AppTheme"
87+
android:exported="true">
88+
</activity>
89+
8090
<meta-data
8191
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
8292
android:value="dev.datlag.mimasu.other.CastOptionsProvider" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.datlag.mimasu
2+
3+
import android.os.Bundle
4+
import androidx.activity.compose.setContent
5+
import dev.datlag.mimasu.ui.space.SpaceContent
6+
import dev.datlag.mimasu.ui.space.SpaceTheme
7+
8+
class SpaceActivity : MimasuActivity() {
9+
10+
override fun onCreate(savedInstanceState: Bundle?) {
11+
super.onCreate(savedInstanceState)
12+
13+
setContent {
14+
SpaceTheme {
15+
SpaceContent()
16+
}
17+
}
18+
}
19+
}
20+

composeApp/src/androidMain/kotlin/dev/datlag/mimasu/common/ExtendPlatform.android.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import androidx.compose.runtime.Composable
1212
import androidx.compose.ui.platform.LocalContext
1313
import androidx.compose.ui.platform.LocalView
1414
import dev.datlag.mimasu.ui.common.findActivity
15+
import dev.datlag.tooling.deleteSafely
16+
import dev.datlag.tooling.existsSafely
17+
import java.io.File
1518

1619
fun Activity.isInPiPMode(): Boolean {
1720
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -59,4 +62,8 @@ fun rememberActivity(
5962
view: View = LocalView.current
6063
): Activity? {
6164
return LocalActivity.current ?: view.context?.findActivity() ?: LocalContext.current.findActivity()
65+
}
66+
67+
fun File.deleteRecursivelySafely(): Boolean = walkBottomUp().fold(true) { res, it ->
68+
(it.deleteSafely(res) || !it.existsSafely(res)) && res
6269
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package dev.datlag.mimasu.other
2+
3+
import android.app.ActivityManager
4+
import android.app.usage.StorageStatsManager
5+
import android.content.Context
6+
import android.os.Build
7+
import android.os.Process
8+
import android.os.storage.StorageManager
9+
import androidx.annotation.RequiresApi
10+
import androidx.core.content.ContextCompat
11+
import androidx.core.content.getSystemService
12+
import dev.datlag.mimasu.common.deleteRecursivelySafely
13+
import dev.datlag.tooling.scopeCatching
14+
import kotlinx.coroutines.async
15+
import kotlinx.coroutines.coroutineScope
16+
import kotlinx.coroutines.flow.MutableStateFlow
17+
import kotlinx.coroutines.flow.asStateFlow
18+
import kotlinx.coroutines.flow.update
19+
import kotlinx.serialization.Serializable
20+
21+
class SpaceManager(
22+
private val context: Context
23+
) {
24+
25+
@RequiresApi(Build.VERSION_CODES.O)
26+
private val storageStatsManager = context.getSystemService<StorageStatsManager>() ?: scopeCatching {
27+
context.getSystemService(Context.STORAGE_STATS_SERVICE) as? StorageStatsManager
28+
}.getOrNull() ?: ContextCompat.getSystemService(context, StorageStatsManager::class.java)
29+
30+
private val storageManager = context.getSystemService<StorageManager>() ?: scopeCatching {
31+
context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
32+
}.getOrNull() ?: ContextCompat.getSystemService(context, StorageManager::class.java)
33+
34+
private val activityManager = context.getSystemService<ActivityManager>() ?: scopeCatching {
35+
context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
36+
}.getOrNull() ?: ContextCompat.getSystemService(context, ActivityManager::class.java)
37+
38+
private val _sizes = MutableStateFlow<Sizes?>(null)
39+
val sizes = _sizes.asStateFlow()
40+
41+
suspend fun loadSizes() {
42+
_sizes.update {
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
44+
modernSizes() ?: legacySizes() ?: it
45+
} else {
46+
legacySizes() ?: it
47+
}
48+
}
49+
}
50+
51+
suspend fun clearCache(): Boolean {
52+
return context.cacheDir.deleteRecursivelySafely().also {
53+
loadSizes()
54+
}
55+
}
56+
57+
suspend fun clearApplicationData(): Boolean {
58+
return activityManager?.clearApplicationUserData()?.let {
59+
it && clearCache()
60+
} ?: clearCache()
61+
}
62+
63+
@RequiresApi(Build.VERSION_CODES.O)
64+
private fun modernSizes(): Sizes? {
65+
val appSpecificStorageUuid = scopeCatching {
66+
storageManager?.getUuidForPath(context.filesDir)
67+
}.getOrNull() ?: return null
68+
val user = Process.myUserHandle()
69+
70+
return scopeCatching {
71+
val storageStats = storageStatsManager?.queryStatsForPackage(
72+
appSpecificStorageUuid,
73+
context.packageName,
74+
user
75+
) ?: return@scopeCatching null
76+
77+
Sizes(
78+
_app = storageStats.appBytes,
79+
_userData = storageStats.dataBytes,
80+
_cache = storageStats.cacheBytes
81+
).takeUnless { it.isEmpty() }
82+
}.getOrNull()
83+
}
84+
85+
private suspend fun legacySizes(): Sizes? = coroutineScope {
86+
val userData = async {
87+
context.filesDir.walkTopDown().sumOf {
88+
scopeCatching {
89+
it.length()
90+
}.getOrNull() ?: 0
91+
}
92+
}
93+
val cache = async {
94+
context.cacheDir.walkTopDown().sumOf {
95+
scopeCatching {
96+
it.length()
97+
}.getOrNull() ?: 0
98+
}
99+
}
100+
101+
return@coroutineScope Sizes(
102+
_app = 0,
103+
_userData = userData.await(),
104+
_cache = cache.await()
105+
).takeUnless { it.isEmpty() }
106+
}
107+
108+
@Serializable
109+
data class Sizes(
110+
private val _app: Long,
111+
private val _userData: Long,
112+
private val _cache: Long
113+
) {
114+
val app: Long = _app.takeIf { it > 0 } ?: 0
115+
val userData: Long = _userData.takeIf { it > 0 } ?: 0
116+
val cache: Long = _cache.takeIf { it > 0 } ?: 0
117+
val total: Long = app + userData + cache
118+
119+
fun isEmpty(): Boolean {
120+
return total <= 0
121+
}
122+
}
123+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package dev.datlag.mimasu.ui.space
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.size
9+
import androidx.compose.foundation.lazy.LazyColumn
10+
import androidx.compose.material3.ButtonDefaults
11+
import androidx.compose.material3.Scaffold
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.collectAsState
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.runtime.remember
16+
import androidx.compose.runtime.rememberCoroutineScope
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.platform.LocalContext
20+
import androidx.compose.ui.unit.dp
21+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
22+
import dev.datlag.mimasu.composeapp.generated.resources.Res
23+
import dev.datlag.mimasu.composeapp.generated.resources.app_name
24+
import dev.datlag.mimasu.other.SpaceManager
25+
import dev.datlag.mimasu.ui.custom.MaterialSymbols
26+
import dev.datlag.mimasu.ui.space.components.SizeInfo
27+
import dev.datlag.tooling.Platform
28+
import dev.datlag.tooling.async.launchVirtualIO
29+
import dev.datlag.tooling.compose.LaunchedVirtualIO
30+
import dev.datlag.tooling.compose.platform.PlatformButton
31+
import dev.datlag.tooling.compose.platform.PlatformButtonColors
32+
import dev.datlag.tooling.compose.platform.PlatformText
33+
import dev.datlag.tooling.compose.platform.colorScheme
34+
import dev.datlag.tooling.compose.platform.rememberIsTv
35+
import dev.datlag.tooling.compose.platform.typography
36+
import io.tolgee.stringResource
37+
38+
@OptIn(MaterialSymbols.RedrawRequired::class)
39+
@Composable
40+
fun SpaceContent() {
41+
Scaffold(
42+
modifier = Modifier.fillMaxSize()
43+
) { padding ->
44+
val context = LocalContext.current
45+
val spaceManager = remember(context) { SpaceManager(context) }
46+
val scope = rememberCoroutineScope()
47+
val sizes by if (Platform.rememberIsTv()) {
48+
spaceManager.sizes.collectAsState()
49+
} else {
50+
spaceManager.sizes.collectAsStateWithLifecycle()
51+
}
52+
53+
LaunchedVirtualIO(spaceManager) {
54+
spaceManager.loadSizes()
55+
}
56+
57+
LazyColumn(
58+
modifier = Modifier.fillMaxSize().padding(padding)
59+
) {
60+
item {
61+
PlatformText(
62+
modifier = Modifier.fillParentMaxWidth().padding(16.dp),
63+
text = stringResource(Res.string.app_name),
64+
style = Platform.typography().headlineLarge,
65+
maxLines = 1
66+
)
67+
}
68+
item {
69+
PlatformText(
70+
modifier = Modifier.fillParentMaxWidth().padding(horizontal = 16.dp),
71+
text = "Free up space by clearing the cache or permanently deleting all data."
72+
)
73+
}
74+
if (sizes != null) {
75+
item {
76+
SizeInfo(
77+
sizes = sizes!!,
78+
modifier = Modifier.fillParentMaxWidth().padding(16.dp)
79+
)
80+
}
81+
}
82+
item {
83+
Row(
84+
modifier = Modifier.fillParentMaxWidth().padding(horizontal = 16.dp),
85+
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
86+
verticalAlignment = Alignment.CenterVertically
87+
) {
88+
PlatformButton(
89+
modifier = Modifier.weight(1F),
90+
onClick = {
91+
scope.launchVirtualIO {
92+
spaceManager.clearApplicationData()
93+
}
94+
},
95+
colors = PlatformButtonColors.default(
96+
containerColor = Platform.colorScheme().error,
97+
contentColor = Platform.colorScheme().onError
98+
)
99+
) {
100+
MaterialSymbols.forcedRedraw(
101+
modifier = Modifier.size(ButtonDefaults.IconSize),
102+
name = MaterialSymbols.DELETE_FOREVER,
103+
contentDescription = null
104+
)
105+
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
106+
PlatformText(text = "Clear Storage")
107+
}
108+
109+
PlatformButton(
110+
modifier = Modifier.weight(1F),
111+
onClick = {
112+
scope.launchVirtualIO {
113+
spaceManager.clearCache()
114+
}
115+
}
116+
) {
117+
MaterialSymbols.forcedRedraw(
118+
modifier = Modifier.size(ButtonDefaults.IconSize),
119+
name = MaterialSymbols.DELETE,
120+
contentDescription = null
121+
)
122+
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
123+
PlatformText(text = "Clear Cache")
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package dev.datlag.mimasu.ui.space
2+
3+
import androidx.compose.foundation.isSystemInDarkTheme
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
6+
import androidx.compose.material3.MaterialExpressiveTheme
7+
import androidx.compose.material3.Typography
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.CompositionLocalProvider
10+
import androidx.compose.ui.Modifier
11+
import dev.datlag.mimasu.ui.LocalDarkMode
12+
import dev.datlag.mimasu.ui.theme.Colors
13+
import dev.datlag.mimasu.ui.theme.dynamicDark
14+
import dev.datlag.mimasu.ui.theme.dynamicLight
15+
import dev.datlag.tooling.Platform
16+
import dev.datlag.tooling.compose.platform.PlatformMaterialTheme
17+
import dev.datlag.tooling.compose.platform.PlatformSurface
18+
import dev.datlag.tooling.compose.platform.colorScheme
19+
import dev.datlag.tooling.compose.platform.rememberIsTv
20+
import dev.datlag.tooling.compose.platform.typography
21+
22+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
23+
@Composable
24+
fun SpaceTheme(
25+
typography: Typography = Platform.typography(),
26+
systemDarkTheme: Boolean = isSystemInDarkTheme() || Platform.rememberIsTv(anyOS = true),
27+
content: @Composable () -> Unit
28+
) {
29+
CompositionLocalProvider(
30+
LocalDarkMode provides systemDarkTheme
31+
) {
32+
PlatformMaterialTheme(
33+
colorScheme = if (systemDarkTheme) Colors.dynamicDark() else Colors.dynamicLight(),
34+
typography = typography
35+
) {
36+
MaterialExpressiveTheme(
37+
colorScheme = Platform.colorScheme(),
38+
typography = Platform.typography()
39+
) {
40+
PlatformSurface(
41+
modifier = Modifier.fillMaxSize(),
42+
containerColor = Platform.colorScheme().background,
43+
contentColor = Platform.colorScheme().onBackground
44+
) {
45+
content()
46+
}
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)