Skip to content

Commit 61ba3a7

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 9262fc0 + 3866163 commit 61ba3a7

13 files changed

Lines changed: 522 additions & 156 deletions

File tree

.claude/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ Detailed guidelines are in `.claude/rules/`:
2323
- `commit-guidelines.md` — Commit message format, PR description format, area prefixes
2424
- `build-commands.md` — Build, test, lint, screenshot, and release commands
2525
- `architecture.md` — Module structure, patterns, base classes, data flow
26-
- `code-style.md` — Kotlin conventions, ViewModel/Fragment patterns, logging
26+
- `code-style.md` — Kotlin conventions, ViewModel/Compose patterns, logging
2727
- `testing.md` — Test locations, patterns, running tests
2828
- `localization.md` — String resources, naming conventions

.claude/rules/architecture.md

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@
77

88
## Core Architecture Patterns
99

10-
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
10+
- **MVVM**: ViewModels with StateFlow for UI state management
1111
- **Dependency Injection**: Hilt/Dagger for dependency management
1212
- **Coroutines**: Extensive use of Kotlin coroutines for async operations
1313
- **Repository Pattern**: Data layer abstraction with sealed State classes
14-
- **Single Activity**: Navigation Component with multiple fragments
14+
- **Single Activity**: Navigation3 with Compose screens
1515

1616
## Base UI Classes (`app/src/main/java/eu/darken/myperm/common/uix/`)
1717

18-
- `ViewModel3`: Full MVVM support with nav events + error events (most feature VMs extend this)
18+
- `ViewModel4`: Primary base class — implements `NavigationEventSource` + `ErrorEventSource2` via `SingleEventFlow` (all
19+
feature VMs extend this)
20+
- `ViewModel3`: Legacy MVVM base with `SingleLiveEvent` (still exists, not used by new code)
1921
- `ViewModel2`: Coroutine scope with error handlers
2022
- `ViewModel1`: Basic logging
21-
- `Fragment3`: MVVM integration, observes navEvents/errorEvents from ViewModel
22-
- `Fragment2`: Lifecycle logging
2323
- `Activity2`: Base activity with logging
24+
- `Service2`: Base service with logging
2425

2526
## Repository Pattern
2627

@@ -38,16 +39,17 @@ sealed class State {
3839

3940
## Navigation System
4041

41-
- Single Activity (`MainActivity`) with `NavHostFragment`
42-
- AndroidX Navigation with Safe Args (KSP-generated)
43-
- Navigation graphs: `res/navigation/main_navigation.xml`, `res/navigation/bottom_navigation.xml`
44-
- `NavEventSource` interface: ViewModels expose `navEvents: SingleLiveEvent<NavDirections>`
45-
- `ErrorEventSource` interface: ViewModels expose `errorEvents: SingleLiveEvent<Throwable>`
42+
- Single Activity (`MainActivity`) with Navigation3 (Compose-based, no fragments)
43+
- Custom `NavigationController` + `NavigationEntry` (not AndroidX Navigation fragments)
44+
- `NavigationEventSource` interface: ViewModels expose `navEvents: SingleEventFlow<NavEvent>`
45+
- `NavEvent` sealed class: `GoTo(destination, popUpTo, inclusive)` and `Up`
46+
- `ErrorEventSource2` interface: ViewModels expose `errorEvents: SingleEventFlow<Throwable>`
4647

4748
## Settings System
4849

49-
- `GeneralSettings` singleton uses SharedPreferences with Moshi JSON serialization
50-
- Flow-based preference reading via `createFlowPreference()`
50+
- `GeneralSettings` singleton uses DataStore Preferences with Kotlinx Serialization
51+
- Flow-based preference reading via `createValue()` with `kotlinxReader`/`kotlinxWriter` helpers
52+
- SharedPreferences retained only for migration via `SharedPreferencesMigration`
5153
- Located in `settings/core/GeneralSettings.kt`
5254

5355
## Data Flow
@@ -57,36 +59,45 @@ The app follows unidirectional data flow:
5759
1. `AppRepo` queries PackageManager for installed apps
5860
2. `PermissionRepo` aggregates permission data from apps
5961
3. ViewModels combine repository flows with filter/sort options
60-
4. UI observes ViewModel state via LiveData
62+
4. Compose UI collects ViewModel state via StateFlow
6163
5. User actions trigger ViewModel methods which update repository or navigate
6264

6365
## Project Structure
6466

67+
Representative structure (not exhaustive):
68+
6569
```
6670
app/src/main/java/eu/darken/myperm/
6771
├── main/ # MainActivity, main navigation hub
6872
├── permissions/ # Permissions feature
6973
│ ├── core/ # PermissionRepo, data models
70-
│ └── ui/ # List and details fragments
74+
│ └── ui/ # List and details Compose screens
7175
├── apps/ # Apps feature
7276
│ ├── core/ # AppRepo, PackageManager interactions
73-
│ └── ui/ # List and details fragments
77+
│ └── ui/ # List and details Compose screens
78+
├── watcher/ # Permission change monitoring
79+
│ ├── core/ # WatcherManager, SnapshotDiffer, PermissionDiff
80+
│ └── ui/ # Dashboard and report detail screens
7481
├── settings/ # Settings feature
7582
│ ├── core/ # GeneralSettings
76-
│ └── ui/ # Settings fragments
83+
│ └── ui/ # Settings Compose screens
7784
└── common/ # Shared utilities
78-
├── uix/ # Base UI classes
85+
├── uix/ # Base UI classes (ViewModel4, Activity2, Service2)
86+
├── compose/ # Shared Compose components
7987
├── coroutine/ # DispatcherProvider, AppScope
8088
├── dagger/ # Hilt DI modules
81-
├── navigation/ # Nav extensions
82-
├── lists/ # ModularAdapter pattern for RecyclerView
83-
└── preferences/# FlowPreference utilities
89+
├── datastore/ # DataStore helpers (createValue, kotlinxReader/Writer)
90+
├── navigation/ # Navigation3 extensions, NavigationEventSource
91+
├── room/ # Room database, DAOs, entities
92+
└── serialization/ # Kotlinx Serialization utilities
8493
```
8594

8695
## Key Dependencies
8796

97+
- **Jetpack Compose**: UI framework (Material 3)
8898
- **Hilt**: Dependency injection framework
89-
- **AndroidX Navigation**: Fragment navigation with SafeArgs
90-
- **Moshi**: JSON serialization for settings and data
99+
- **Navigation3**: Compose-based navigation
100+
- **Kotlinx Serialization**: JSON serialization for settings and data
101+
- **DataStore**: Preferences storage
102+
- **Room**: Database for watcher snapshots
91103
- **Coil**: Image loading for app icons
92-
- **Material Design**: UI components

.claude/rules/code-style.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,22 @@ Use `DispatcherProvider` interface for testability instead of hardcoded dispatch
2222

2323
```kotlin
2424
@HiltViewModel
25-
class MyFeatureVM @Inject constructor(
25+
class MyFeatureViewModel @Inject constructor(
2626
private val handle: SavedStateHandle,
2727
dispatcherProvider: DispatcherProvider,
2828
private val myRepo: MyRepo,
29-
) : ViewModel3(dispatcherProvider = dispatcherProvider)
29+
) : ViewModel4(dispatcherProvider = dispatcherProvider)
3030
```
3131

32-
## Fragment Creation Pattern
32+
## Screen Creation Pattern
3333

3434
```kotlin
35-
@AndroidEntryPoint
36-
class MyFeatureFragment : Fragment3(R.layout.my_feature_fragment) {
37-
override val vm: MyFeatureVM by viewModels()
38-
override val ui: MyFeatureFragmentBinding by viewBinding()
35+
@Composable
36+
fun MyFeatureScreenHost(vm: MyFeatureViewModel = hiltViewModel()) {
37+
ErrorEventHandler(vm)
38+
NavigationEventHandler(vm)
39+
val state by vm.state.collectAsState()
40+
MyFeatureScreen(state = state, ...)
3941
}
4042
```
4143

@@ -58,19 +60,18 @@ log(TAG, WARN) { "Unexpected state" } // WARN
5860

5961
## UI Patterns
6062

61-
- XML layouts with ViewBinding for UI components
63+
- Jetpack Compose is the sole UI framework — no XML layouts remain
6264
- Material 3 theming and design system
63-
- Single Activity architecture with Fragment-based navigation
64-
- Compose is used for new screens alongside existing XML/ViewBinding
65+
- Single Activity architecture with Compose-based navigation
6566

6667
## Error Handling
6768

68-
- Use the established error handling patterns with `ErrorEventSource`
69-
- ViewModels expose `errorEvents: SingleLiveEvent<Throwable>`
69+
- ViewModels implement `ErrorEventSource2`, exposing `errorEvents: SingleEventFlow<Throwable>`
70+
- Compose screens wire errors via `ErrorEventHandler(vm)` composable
7071

7172
## Data & State
7273

7374
- Reactive programming with Kotlin Flow and StateFlow
74-
- SharedPreferences with Moshi JSON serialization for settings
75+
- DataStore with Kotlinx Serialization for settings
7576
- Room for database operations
7677
- Coil for image loading

app/src/main/java/eu/darken/myperm/apps/ui/list/AppsFilterOptions.kt

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,55 @@ data class AppsFilterOptions(
136136
SECONDARY_PROFILE(
137137
group = Group.PROFILE,
138138
labelRes = R.string.apps_filter_profile_secondary_label,
139-
matches = { it.pkgType == PkgType.SECONDARY_PROFILE }
139+
matches = { it.pkgType == PkgType.SECONDARY_PROFILE || it.pkgType == PkgType.SECONDARY_USER }
140+
),
141+
@SerialName("CAMERA")
142+
CAMERA(
143+
group = Group.PROPERTIES,
144+
labelRes = R.string.apps_filter_camera_label,
145+
matches = { app ->
146+
app.requestedPermissions.any {
147+
it.permissionId == "android.permission.CAMERA" && it.status.isGranted
148+
}
149+
}
150+
),
151+
@SerialName("LOCATION")
152+
LOCATION(
153+
group = Group.PROPERTIES,
154+
labelRes = R.string.apps_filter_location_label,
155+
matches = { app ->
156+
app.requestedPermissions.any {
157+
(it.permissionId == "android.permission.ACCESS_FINE_LOCATION"
158+
|| it.permissionId == "android.permission.ACCESS_COARSE_LOCATION")
159+
&& it.status.isGranted
160+
}
161+
}
162+
),
163+
@SerialName("MICROPHONE")
164+
MICROPHONE(
165+
group = Group.PROPERTIES,
166+
labelRes = R.string.apps_filter_microphone_label,
167+
matches = { app ->
168+
app.requestedPermissions.any {
169+
it.permissionId == "android.permission.RECORD_AUDIO" && it.status.isGranted
170+
}
171+
}
172+
),
173+
@SerialName("CONTACTS")
174+
CONTACTS(
175+
group = Group.PROPERTIES,
176+
labelRes = R.string.apps_filter_contacts_label,
177+
matches = { app ->
178+
app.requestedPermissions.any {
179+
it.permissionId == "android.permission.READ_CONTACTS" && it.status.isGranted
180+
}
181+
}
182+
),
183+
@SerialName("OLD_API_TARGET")
184+
OLD_API_TARGET(
185+
group = Group.PROPERTIES,
186+
labelRes = R.string.apps_filter_old_api_target_label,
187+
matches = { it.apiTargetLevel != null && it.apiTargetLevel < OLD_API_THRESHOLD }
140188
),
141189
;
142190
}
@@ -147,4 +195,8 @@ data class AppsFilterOptions(
147195
groupFilters.any { it.matches(app) }
148196
}
149197
}
198+
199+
companion object {
200+
const val OLD_API_THRESHOLD = 29 // Android 10 (Q)
201+
}
150202
}

app/src/main/java/eu/darken/myperm/apps/ui/list/AppsScreen.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package eu.darken.myperm.apps.ui.list
22

33
import androidx.compose.animation.AnimatedVisibility
44
import androidx.compose.animation.expandVertically
5+
import androidx.compose.animation.fadeIn
6+
import androidx.compose.animation.fadeOut
57
import androidx.compose.animation.shrinkVertically
8+
import androidx.compose.animation.slideInVertically
9+
import androidx.compose.animation.slideOutVertically
610
import androidx.compose.foundation.clickable
711
import androidx.compose.foundation.layout.Arrangement
812
import androidx.compose.foundation.layout.Box
@@ -44,6 +48,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
4448
import androidx.compose.runtime.setValue
4549
import androidx.compose.ui.Alignment
4650
import androidx.compose.ui.Modifier
51+
import androidx.compose.ui.input.nestedscroll.nestedScroll
4752
import androidx.compose.ui.layout.onSizeChanged
4853
import androidx.compose.ui.platform.LocalDensity
4954
import androidx.compose.ui.res.pluralStringResource
@@ -60,6 +65,7 @@ import eu.darken.myperm.common.compose.AppIcon
6065
import eu.darken.myperm.common.compose.Preview2
6166
import eu.darken.myperm.common.compose.PreviewWrapper
6267
import eu.darken.myperm.common.compose.SearchTextField
68+
import eu.darken.myperm.common.compose.rememberFabVisibility
6369
import androidx.compose.runtime.collectAsState
6470
import eu.darken.myperm.common.error.ErrorEventHandler
6571
import eu.darken.myperm.common.navigation.NavigationEventHandler
@@ -119,16 +125,24 @@ fun AppsScreen(
119125
var searchQuery by rememberSaveable { mutableStateOf("") }
120126
var isSearchActive by rememberSaveable { mutableStateOf(false) }
121127
var showOverflowMenu by rememberSaveable { mutableStateOf(false) }
128+
val (fabVisible, scrollConnection) = rememberFabVisibility()
122129

123130
Scaffold(
131+
modifier = Modifier.nestedScroll(scrollConnection),
124132
floatingActionButton = {
125-
FloatingActionButton(
126-
onClick = { if (!isRefreshing) onRefresh() },
133+
AnimatedVisibility(
134+
visible = fabVisible,
135+
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
136+
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
127137
) {
128-
if (isRefreshing) {
129-
CircularProgressIndicator(modifier = Modifier.size(24.dp))
130-
} else {
131-
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.general_refresh_action))
138+
FloatingActionButton(
139+
onClick = { if (!isRefreshing) onRefresh() },
140+
) {
141+
if (isRefreshing) {
142+
CircularProgressIndicator(modifier = Modifier.size(24.dp))
143+
} else {
144+
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.general_refresh_action))
145+
}
132146
}
133147
}
134148
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package eu.darken.myperm.common.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableFloatStateOf
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
11+
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
12+
13+
@Composable
14+
fun rememberFabVisibility(threshold: Float = 10f): Pair<Boolean, NestedScrollConnection> {
15+
var visible by remember { mutableStateOf(true) }
16+
var cumDelta by remember { mutableFloatStateOf(0f) }
17+
18+
val connection = remember {
19+
object : NestedScrollConnection {
20+
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
21+
cumDelta += available.y
22+
if (cumDelta > threshold) {
23+
visible = true
24+
cumDelta = 0f
25+
} else if (cumDelta < -threshold) {
26+
visible = false
27+
cumDelta = 0f
28+
}
29+
return Offset.Zero
30+
}
31+
}
32+
}
33+
34+
return visible to connection
35+
}

app/src/main/java/eu/darken/myperm/main/ui/overview/OverviewPreviewData.kt

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package eu.darken.myperm.main.ui.overview
22

3+
import eu.darken.myperm.main.ui.overview.OverviewViewModel.SummaryCategory
4+
35
internal object OverviewPreviewData {
46

57
fun loadedState() = OverviewViewModel.State(
@@ -9,21 +11,24 @@ internal object OverviewPreviewData {
911
patchLevel = "2026-01-05",
1012
),
1113
summaryInfo = OverviewViewModel.SummaryInfo(
12-
activeProfileUser = 9,
13-
activeProfileSystem = 339,
14-
otherProfileUser = 0,
15-
otherProfileSystem = 1,
16-
sideloaded = 5,
17-
installerAppsUser = 0,
18-
installerAppsSystem = 1,
19-
systemAlertWindowUser = 0,
20-
systemAlertWindowSystem = 5,
21-
noInternetUser = 2,
22-
noInternetSystem = 1,
23-
clonesUser = 0,
24-
clonesSystem = 0,
25-
sharedIdsUser = 0,
26-
sharedIdsSystem = 39,
14+
counts = mapOf(
15+
SummaryCategory.ACTIVE_PROFILE to PkgCount(9, 339),
16+
SummaryCategory.OTHER_PROFILES to PkgCount(0, 1),
17+
SummaryCategory.CLONES to PkgCount(0, 0),
18+
SummaryCategory.GOOGLE_PLAY to PkgCount(6, 0),
19+
SummaryCategory.OEM_STORE to PkgCount(1, 0),
20+
SummaryCategory.SIDELOADED to PkgCount(5, 0),
21+
SummaryCategory.CAMERA to PkgCount(4, 2),
22+
SummaryCategory.LOCATION to PkgCount(7, 5),
23+
SummaryCategory.MICROPHONE to PkgCount(3, 1),
24+
SummaryCategory.CONTACTS to PkgCount(5, 3),
25+
SummaryCategory.INSTALLERS to PkgCount(0, 1),
26+
SummaryCategory.OVERLAYERS to PkgCount(0, 5),
27+
SummaryCategory.NO_INTERNET to PkgCount(2, 1),
28+
SummaryCategory.SHARED_IDS to PkgCount(0, 39),
29+
SummaryCategory.BATTERY_OPT to PkgCount(3, 12),
30+
SummaryCategory.OLD_API to PkgCount(1, 8),
31+
),
2732
),
2833
versionDesc = "v1.2.3 (42) ~ abc1234/foss/debug",
2934
isLoading = false,

0 commit comments

Comments
 (0)