From 9a0f519141ddd9f7fbf27c152cba88239ffeb914 Mon Sep 17 00:00:00 2001 From: ikodbapc Date: Tue, 13 Jan 2026 14:33:52 +0100 Subject: [PATCH 01/17] to compose, not finished --- .idea/.gitignore | 3 + .idea/caches/deviceStreaming.xml | 1258 +++++++++++++++++ .idea/markdown.xml | 8 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + project/.claude/compose-migration-plan.md | 282 ++++ project/.claude/settings.local.json | 11 + project/app/build.gradle.kts | 10 + project/app/gradle.properties | 2 +- .../android/ui/welcome/PlayServicesPage.kt | 161 +++ .../android/ui/welcome/WelcomeActivity.kt | 49 +- .../ui/welcome/fragments/PlayFragment.kt | 89 -- .../fragments/PlayFragmentViewModel.kt | 26 - .../src/gms/res/layout/ui_welcome_play.xml | 77 - .../android/support/BindingAdapters.kt | 46 + .../android/ui/contacts/ContactsActivity.kt | 171 ++- .../android/ui/contacts/ContactsScreen.kt | 205 +++ .../owntracks/android/ui/map/MapActivity.kt | 45 +- .../android/ui/navigation/BottomNavBar.kt | 76 + .../android/ui/navigation/Destinations.kt | 50 + .../android/ui/navigation/OwnTracksNavHost.kt | 110 ++ .../android/ui/preferences/LicenseFragment.kt | 11 - .../ui/preferences/PreferencesActivity.kt | 64 +- .../ui/preferences/PreferencesFragment.kt | 19 + .../ui/preferences/about/AboutActivity.kt | 57 +- .../ui/preferences/about/AboutFragment.kt | 62 - .../ui/preferences/about/AboutScreen.kt | 254 ++++ .../ui/preferences/about/LicensesScreen.kt | 133 ++ .../ui/preferences/editor/EditorActivity.kt | 279 ++-- .../ui/preferences/editor/EditorScreen.kt | 255 ++++ .../ui/preferences/load/LoadActivity.kt | 214 ++- .../android/ui/preferences/load/LoadScreen.kt | 127 ++ .../android/ui/status/StatusActivity.kt | 148 +- .../android/ui/status/StatusScreen.kt | 268 ++++ .../android/ui/status/logs/LogEntryAdapter.kt | 92 -- .../android/ui/status/logs/LogPalette.kt | 10 - .../ui/status/logs/LogViewerActivity.kt | 213 ++- .../android/ui/status/logs/LogViewerScreen.kt | 235 +++ .../org/owntracks/android/ui/theme/Color.kt | 85 ++ .../org/owntracks/android/ui/theme/Theme.kt | 94 ++ .../org/owntracks/android/ui/theme/Type.kt | 115 ++ .../android/ui/waypoint/WaypointActivity.kt | 226 +-- .../android/ui/waypoint/WaypointScreen.kt | 207 +++ .../android/ui/waypoints/WaypointsActivity.kt | 184 +-- .../android/ui/waypoints/WaypointsScreen.kt | 201 +++ .../android/ui/welcome/BaseWelcomeActivity.kt | 191 +-- .../android/ui/welcome/WelcomeAdapter.kt | 23 - .../android/ui/welcome/WelcomeScreen.kt | 588 ++++++++ .../android/ui/welcome/WelcomeViewModel.kt | 57 - .../fragments/ConnectionSetupFragment.kt | 49 - .../ui/welcome/fragments/FinishFragment.kt | 41 - .../ui/welcome/fragments/IntroFragment.kt | 29 - .../fragments/LocationPermissionFragment.kt | 94 -- .../NotificationPermissionFragment.kt | 73 - .../ui/welcome/fragments/WelcomeFragment.kt | 12 - .../src/main/res/layout/log_viewer_entry.xml | 17 - .../app/src/main/res/layout/ui_contacts.xml | 55 - project/app/src/main/res/layout/ui_map.xml | 22 +- .../src/main/res/layout/ui_preferences.xml | 27 +- .../main/res/layout/ui_preferences_editor.xml | 36 - .../layout/ui_preferences_editor_dialog.xml | 45 - .../main/res/layout/ui_preferences_load.xml | 68 - .../main/res/layout/ui_preferences_logs.xml | 46 - project/app/src/main/res/layout/ui_status.xml | 240 ---- .../app/src/main/res/layout/ui_waypoint.xml | 105 -- .../app/src/main/res/layout/ui_waypoints.xml | 53 - .../app/src/main/res/layout/ui_welcome.xml | 65 - .../layout/ui_welcome_connection_setup.xml | 63 - .../src/main/res/layout/ui_welcome_finish.xml | 58 - .../src/main/res/layout/ui_welcome_intro.xml | 55 - .../layout/ui_welcome_location_permission.xml | 74 - .../ui_welcome_notification_permission.xml | 65 - .../app/src/main/res/menu/bottom_nav_menu.xml | 19 + project/app/src/main/res/values/strings.xml | 2 + project/app/src/main/res/xml/about.xml | 79 -- .../src/main/res/xml/preferences_licenses.xml | 101 -- .../app/src/main/res/xml/preferences_root.xml | 14 + .../android/ui/welcome/WelcomeActivity.kt | 21 +- project/gradle/libs.versions.toml | 19 + project/tmpclaude-0bd4-cwd | 1 + project/tmpclaude-1108-cwd | 1 + project/tmpclaude-11ed-cwd | 1 + project/tmpclaude-1238-cwd | 1 + project/tmpclaude-13f7-cwd | 1 + project/tmpclaude-1ea3-cwd | 1 + project/tmpclaude-2332-cwd | 1 + project/tmpclaude-26a2-cwd | 1 + project/tmpclaude-2ab9-cwd | 1 + project/tmpclaude-3067-cwd | 1 + project/tmpclaude-34dd-cwd | 1 + project/tmpclaude-3bec-cwd | 1 + project/tmpclaude-3de1-cwd | 1 + project/tmpclaude-3e45-cwd | 1 + project/tmpclaude-3f91-cwd | 1 + project/tmpclaude-4095-cwd | 1 + project/tmpclaude-40ba-cwd | 1 + project/tmpclaude-4a22-cwd | 1 + project/tmpclaude-4fe6-cwd | 1 + project/tmpclaude-5b88-cwd | 1 + project/tmpclaude-5d4b-cwd | 1 + project/tmpclaude-5da1-cwd | 1 + project/tmpclaude-5dc6-cwd | 1 + project/tmpclaude-5f1a-cwd | 1 + project/tmpclaude-5f79-cwd | 1 + project/tmpclaude-6c09-cwd | 1 + project/tmpclaude-6c62-cwd | 1 + project/tmpclaude-712e-cwd | 1 + project/tmpclaude-7795-cwd | 1 + project/tmpclaude-7a12-cwd | 1 + project/tmpclaude-85e7-cwd | 1 + project/tmpclaude-8ae3-cwd | 1 + project/tmpclaude-9a5b-cwd | 1 + project/tmpclaude-9a95-cwd | 1 + project/tmpclaude-9b05-cwd | 1 + project/tmpclaude-9d79-cwd | 1 + project/tmpclaude-b69e-cwd | 1 + project/tmpclaude-b817-cwd | 1 + project/tmpclaude-b93e-cwd | 1 + project/tmpclaude-b97f-cwd | 1 + project/tmpclaude-bc86-cwd | 1 + project/tmpclaude-c51a-cwd | 1 + project/tmpclaude-c53c-cwd | 1 + project/tmpclaude-c714-cwd | 1 + project/tmpclaude-c958-cwd | 1 + project/tmpclaude-ca6a-cwd | 1 + project/tmpclaude-ca93-cwd | 1 + project/tmpclaude-cc24-cwd | 1 + project/tmpclaude-d2db-cwd | 1 + project/tmpclaude-d357-cwd | 1 + project/tmpclaude-d35f-cwd | 1 + project/tmpclaude-da4d-cwd | 1 + project/tmpclaude-dc02-cwd | 1 + project/tmpclaude-dcab-cwd | 1 + project/tmpclaude-e4f3-cwd | 1 + project/tmpclaude-e869-cwd | 1 + project/tmpclaude-f0ac-cwd | 1 + project/tmpclaude-f226-cwd | 1 + project/tmpclaude-f522-cwd | 1 + 139 files changed, 5764 insertions(+), 3053 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/caches/deviceStreaming.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 project/.claude/compose-migration-plan.md create mode 100644 project/.claude/settings.local.json create mode 100644 project/app/src/gms/java/org/owntracks/android/ui/welcome/PlayServicesPage.kt delete mode 100644 project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragment.kt delete mode 100644 project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragmentViewModel.kt delete mode 100644 project/app/src/gms/res/layout/ui_welcome_play.xml create mode 100644 project/app/src/main/java/org/owntracks/android/support/BindingAdapters.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/contacts/ContactsScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/navigation/BottomNavBar.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/navigation/Destinations.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/navigation/OwnTracksNavHost.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/LicenseFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/about/AboutFragment.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/about/AboutScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/about/LicensesScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/editor/EditorScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/preferences/load/LoadScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/status/StatusScreen.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/status/logs/LogEntryAdapter.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/status/logs/LogPalette.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/status/logs/LogViewerScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/theme/Color.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/theme/Theme.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/theme/Type.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/waypoint/WaypointScreen.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/waypoints/WaypointsScreen.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/WelcomeAdapter.kt create mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/WelcomeScreen.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/WelcomeViewModel.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/ConnectionSetupFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/FinishFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/IntroFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/LocationPermissionFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/NotificationPermissionFragment.kt delete mode 100644 project/app/src/main/java/org/owntracks/android/ui/welcome/fragments/WelcomeFragment.kt delete mode 100644 project/app/src/main/res/layout/log_viewer_entry.xml delete mode 100644 project/app/src/main/res/layout/ui_contacts.xml delete mode 100644 project/app/src/main/res/layout/ui_preferences_editor.xml delete mode 100644 project/app/src/main/res/layout/ui_preferences_editor_dialog.xml delete mode 100644 project/app/src/main/res/layout/ui_preferences_load.xml delete mode 100644 project/app/src/main/res/layout/ui_preferences_logs.xml delete mode 100644 project/app/src/main/res/layout/ui_status.xml delete mode 100644 project/app/src/main/res/layout/ui_waypoint.xml delete mode 100644 project/app/src/main/res/layout/ui_waypoints.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome_connection_setup.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome_finish.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome_intro.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome_location_permission.xml delete mode 100644 project/app/src/main/res/layout/ui_welcome_notification_permission.xml create mode 100644 project/app/src/main/res/menu/bottom_nav_menu.xml delete mode 100644 project/app/src/main/res/xml/about.xml delete mode 100644 project/app/src/main/res/xml/preferences_licenses.xml create mode 100644 project/tmpclaude-0bd4-cwd create mode 100644 project/tmpclaude-1108-cwd create mode 100644 project/tmpclaude-11ed-cwd create mode 100644 project/tmpclaude-1238-cwd create mode 100644 project/tmpclaude-13f7-cwd create mode 100644 project/tmpclaude-1ea3-cwd create mode 100644 project/tmpclaude-2332-cwd create mode 100644 project/tmpclaude-26a2-cwd create mode 100644 project/tmpclaude-2ab9-cwd create mode 100644 project/tmpclaude-3067-cwd create mode 100644 project/tmpclaude-34dd-cwd create mode 100644 project/tmpclaude-3bec-cwd create mode 100644 project/tmpclaude-3de1-cwd create mode 100644 project/tmpclaude-3e45-cwd create mode 100644 project/tmpclaude-3f91-cwd create mode 100644 project/tmpclaude-4095-cwd create mode 100644 project/tmpclaude-40ba-cwd create mode 100644 project/tmpclaude-4a22-cwd create mode 100644 project/tmpclaude-4fe6-cwd create mode 100644 project/tmpclaude-5b88-cwd create mode 100644 project/tmpclaude-5d4b-cwd create mode 100644 project/tmpclaude-5da1-cwd create mode 100644 project/tmpclaude-5dc6-cwd create mode 100644 project/tmpclaude-5f1a-cwd create mode 100644 project/tmpclaude-5f79-cwd create mode 100644 project/tmpclaude-6c09-cwd create mode 100644 project/tmpclaude-6c62-cwd create mode 100644 project/tmpclaude-712e-cwd create mode 100644 project/tmpclaude-7795-cwd create mode 100644 project/tmpclaude-7a12-cwd create mode 100644 project/tmpclaude-85e7-cwd create mode 100644 project/tmpclaude-8ae3-cwd create mode 100644 project/tmpclaude-9a5b-cwd create mode 100644 project/tmpclaude-9a95-cwd create mode 100644 project/tmpclaude-9b05-cwd create mode 100644 project/tmpclaude-9d79-cwd create mode 100644 project/tmpclaude-b69e-cwd create mode 100644 project/tmpclaude-b817-cwd create mode 100644 project/tmpclaude-b93e-cwd create mode 100644 project/tmpclaude-b97f-cwd create mode 100644 project/tmpclaude-bc86-cwd create mode 100644 project/tmpclaude-c51a-cwd create mode 100644 project/tmpclaude-c53c-cwd create mode 100644 project/tmpclaude-c714-cwd create mode 100644 project/tmpclaude-c958-cwd create mode 100644 project/tmpclaude-ca6a-cwd create mode 100644 project/tmpclaude-ca93-cwd create mode 100644 project/tmpclaude-cc24-cwd create mode 100644 project/tmpclaude-d2db-cwd create mode 100644 project/tmpclaude-d357-cwd create mode 100644 project/tmpclaude-d35f-cwd create mode 100644 project/tmpclaude-da4d-cwd create mode 100644 project/tmpclaude-dc02-cwd create mode 100644 project/tmpclaude-dcab-cwd create mode 100644 project/tmpclaude-e4f3-cwd create mode 100644 project/tmpclaude-e869-cwd create mode 100644 project/tmpclaude-f0ac-cwd create mode 100644 project/tmpclaude-f226-cwd create mode 100644 project/tmpclaude-f522-cwd diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000000..51d2b6186d --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1258 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000000..c61ea3346e --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..639900d13c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..0eaad78594 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/project/.claude/compose-migration-plan.md b/project/.claude/compose-migration-plan.md new file mode 100644 index 0000000000..d265cf6d91 --- /dev/null +++ b/project/.claude/compose-migration-plan.md @@ -0,0 +1,282 @@ +# OwnTracks Compose Migration Plan + +## Overview +Migrating OwnTracks Android app from Activities/Fragments with XML layouts to Jetpack Compose with Navigation Compose. The drawer navigation has been replaced with bottom navigation. + +## Current Status + +### Completed Infrastructure +- [x] Compose dependencies added to `libs.versions.toml` +- [x] Compose enabled in `app/build.gradle.kts` +- [x] Theme files created (`ui/theme/Color.kt`, `Theme.kt`, `Type.kt`) +- [x] Navigation infrastructure (`ui/navigation/Destinations.kt`, `OwnTracksNavHost.kt`) +- [x] ~~Compose drawer component (`ui/navigation/AppDrawer.kt`)~~ (deleted - was deprecated) +- [x] Bottom navigation component (`ui/navigation/BottomNavBar.kt`) +- [x] Bottom nav menu (`res/menu/bottom_nav_menu.xml`) + +### Bottom Navigation Migration (COMPLETED) +Replaced drawer navigation with 4-tab bottom navigation across all main screens: + +| Tab | Icon | Screen | +|-----|------|--------| +| Map | map | Main map view | +| Contacts | people | Contact list | +| Waypoints | location_on | Regions/geofences | +| Preferences | settings | Settings + Status + About | + +**Changes made:** +- Created `BottomNavBar.kt` Compose component +- Created `bottom_nav_menu.xml` menu resource +- Updated `MapActivity` - removed DrawerLayout, added BottomNavigationView +- Updated `ContactsActivity/ContactsScreen` - replaced drawer with BottomNavBar +- Updated `WaypointsActivity/WaypointsScreen` - replaced drawer with BottomNavBar +- Updated `PreferencesActivity` - removed drawer, added BottomNavigationView +- Updated `StatusActivity/StatusScreen` - removed drawer, now uses back navigation (accessed from Preferences) +- Added Status, About, Exit to preferences_root.xml as menu items +- Updated `PreferencesFragment` to handle Status/About/Exit clicks + +### Migrated Screens + +#### 1. Status Screen (Now accessed via Preferences) +- **Files**: `StatusActivity.kt`, `StatusScreen.kt` +- **Deleted**: `ui_status.xml` +- **Features**: Status items, battery optimization dialog, location permissions dialog, view logs button +- **Navigation**: Back button (accessed from Preferences > Status) + +#### 2. Contacts Screen +- **Files**: `ContactsActivity.kt`, `ContactsScreen.kt` +- **Deleted**: `ui_contacts.xml` +- **Features**: LazyColumn with contacts, contact avatar loading via `ContactImageBindingAdapter`, relative timestamps, geocoded locations +- **Navigation**: Bottom navigation bar + +#### 3. Waypoints Screen +- **Files**: `WaypointsActivity.kt`, `WaypointsScreen.kt` +- **Deleted**: `ui_waypoints.xml` +- **Features**: LazyColumn with waypoints, overflow menu (import/export), add button, geofence transition status +- **Navigation**: Bottom navigation bar + +#### 4. About Screen (Accessed via Preferences) +- **Files**: `AboutActivity.kt`, `AboutScreen.kt`, `LicensesScreen.kt` +- **Deleted**: `AboutFragment.kt`, `LicenseFragment.kt`, `about.xml`, `preferences_licenses.xml` +- **Features**: + - Version info with changelog link + - Documentation, License, Source Code links + - Libraries/Licenses screen (internal navigation) + - Translations with plural string support + - Feedback section (Issues, Mastodon) +- **Navigation**: Back button (accessed from Preferences > About) + +### Screens Using View System (with Bottom Nav) + +#### 5. Preferences Screen +- **Current**: `PreferencesActivity.kt`, multiple `*Fragment.kt` files +- **Layout**: `ui_preferences.xml` with BottomNavigationView +- **Complexity**: High - uses PreferenceFragmentCompat with XML preferences +- **Navigation**: Bottom navigation bar +- **Contains**: Status, About, and Exit menu items + +#### 6. Map Screen +- **Current**: `MapActivity.kt`, `MapFragment.kt` (GMS/OSS variants), `ui_map.xml` +- **Layout**: CoordinatorLayout with BottomNavigationView (drawer removed) +- **Navigation**: Bottom navigation bar +- **Features**: Map fragment, bottom sheet for contact details, FABs + +### Secondary Screens (Back Navigation) + +##### 7.1 LogViewerActivity (Migrated) +- **Files**: `LogViewerActivity.kt`, `LogViewerScreen.kt` +- **Deleted**: `LogEntryAdapter.kt`, `LogPalette.kt`, `ui_preferences_logs.xml`, `log_viewer_entry.xml` +- **Features**: + - LazyColumn with log entries (horizontally scrollable) + - Color-coded log levels (debug, info, warning, error) + - Auto-scroll to bottom when new logs arrive + - Overflow menu (toggle debug logs, clear) + - FAB for sharing/exporting logs + - Back button navigation + +##### 7.2 WaypointActivity (Migrated) +- **Files**: `WaypointActivity.kt`, `WaypointScreen.kt` +- **Deleted**: `ui_waypoint.xml` +- **Features**: + - Form with OutlinedTextField for description, latitude, longitude, radius + - Input validation with error states + - Save/Delete action buttons in TopAppBar + - Delete confirmation dialog + - Back button navigation +- **Note**: Moved `@BindingAdapter` functions for `relativeTimeSpanString` to `support/BindingAdapters.kt` (still used by ui_row_contact.xml and ui_row_waypoint.xml) + +##### 7.3 WelcomeActivity (Migrated) +- **Files**: `BaseWelcomeActivity.kt`, `WelcomeScreen.kt`, `PlayServicesPage.kt` (gms) +- **Deleted**: + - Layouts: `ui_welcome.xml`, `ui_welcome_intro.xml`, `ui_welcome_connection_setup.xml`, `ui_welcome_location_permission.xml`, `ui_welcome_notification_permission.xml`, `ui_welcome_finish.xml`, `ui_welcome_play.xml` + - Kotlin: `WelcomeAdapter.kt`, `WelcomeViewModel.kt`, `WelcomeFragment.kt`, `IntroFragment.kt`, `ConnectionSetupFragment.kt`, `LocationPermissionFragment.kt`, `NotificationPermissionFragment.kt`, `FinishFragment.kt`, `PlayFragment.kt`, `PlayFragmentViewModel.kt` +- **Features**: + - HorizontalPager for swipeable onboarding pages + - Page indicator dots + - Permission requests with `rememberLauncherForActivityResult` + - GMS/OSS variants with different page lists + - Play Services check page (GMS only) + - Dynamic page list based on API level (NotificationPermission for Android 13+) + +##### 7.4 EditorActivity (Migrated) +- **Files**: `EditorActivity.kt`, `EditorScreen.kt` +- **Deleted**: `ui_preferences_editor.xml`, `ui_preferences_editor_dialog.xml` +- **Features**: + - Display effective configuration as JSON + - Overflow menu (export, import file, edit single value) + - Snackbar for feedback messages + - Preference editor dialog with autocomplete for keys (ExposedDropdownMenuBox) + - Export configuration to file picker + - Back button navigation + +##### 7.5 LoadActivity (Migrated) +- **Files**: `LoadActivity.kt`, `LoadScreen.kt` +- **Deleted**: `ui_preferences_load.xml` +- **Features**: + - Loading state with CircularProgressIndicator + - Success state showing imported configuration JSON + - Failed state showing error message + - Close/Save action buttons based on import status + - Handles ACTION_VIEW intents, content URIs, owntracks:// scheme + - File picker for manual import + +## Established Patterns + +### Activity Structure with Bottom Nav (Compose) +```kotlin +@AndroidEntryPoint +class SomeActivity : AppCompatActivity() { + private val viewModel: SomeViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + OwnTracksTheme { + SomeScreen( + viewModel = viewModel, + onNavigate = { destination -> + navigateToDestination(destination) + }, + // ... other callbacks + ) + } + } + } + + private fun navigateToDestination(destination: Destination) { + val activityClass = destination.toActivityClass() ?: return + if (this.javaClass != activityClass) { + startActivity(Intent(this, activityClass)) + } + } +} +``` + +### Screen Composable Structure with Bottom Nav +```kotlin +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SomeScreen( + // State from ViewModel + someData: List, + // Callbacks + onNavigate: (Destination) -> Unit, + onItemClick: (Item) -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title)) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary + ) + ) + }, + bottomBar = { + BottomNavBar( + currentDestination = Destination.SomeScreen, + onNavigate = onNavigate + ) + }, + modifier = modifier + ) { paddingValues -> + // Content + } +} +``` + +### Secondary Screen with Back Navigation +```kotlin +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SecondaryScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) + } + }, + colors = TopAppBarDefaults.topAppBarColors(...) + ) + }, + modifier = modifier + ) { paddingValues -> + // Content + } +} +``` + +### State Collection +- StateFlow: `val state by viewModel.stateFlow.collectAsStateWithLifecycle()` +- LiveData: `val state by viewModel.liveData.observeAsState(initial = defaultValue)` +- Flow events: Use `LaunchedEffect` to collect + +## Future Work + +### Potential Improvements +1. **Single Activity Architecture**: Consolidate all screens into a single MainActivity with NavHost +2. **Map Screen Compose Migration**: Phase 2-4 as outlined below +3. **Preferences Screen Compose Migration**: Build custom Compose preferences UI + +### Map Screen Migration Phases (Optional) +1. ~~**Phase 1 (Completed)**: Added bottom navigation, removed drawer~~ +2. **Phase 2 (Partial migration)**: Migrate bottom sheet content to Compose ModalBottomSheet +3. **Phase 3 (FABs)**: Migrate FABs to Compose FloatingActionButton +4. **Phase 4 (Full migration)**: Convert to single-Activity with NavHost + +## Build Command +```bash +./gradlew assembleGmsDebug --no-daemon -Dorg.gradle.java.home="C:\Program Files\Java\jdk-22" +``` + +## Dependencies Added +```toml +# In libs.versions.toml +compose-bom = "2024.12.01" +compose-compiler = "1.5.15" +navigation-compose = "2.8.5" +lifecycle-runtime-compose = "2.8.7" + +# Libraries +compose-bom, compose-ui, compose-ui-graphics, compose-ui-tooling, +compose-ui-tooling-preview, compose-material3, compose-material-icons-extended, +compose-foundation, compose-runtime-livedata, activity-compose, +navigation-compose, lifecycle-runtime-compose +``` + +## Notes +- Kotlin version is 1.9.25, requires Compose Compiler 1.5.x (not 2.x) +- DataBinding is still enabled for non-migrated screens (MapActivity, PreferencesActivity) +- ViewModels remain unchanged - they work with both View system and Compose +- Hilt injection unchanged - @AndroidEntryPoint still works +- AppDrawer.kt deleted - bottom navigation is now used instead diff --git a/project/.claude/settings.local.json b/project/.claude/settings.local.json new file mode 100644 index 0000000000..44a67c3065 --- /dev/null +++ b/project/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(./gradlew assembleGmsDebug:*)", + "Bash(./gradlew dependencies:*)", + "Bash(where java:*)", + "Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-22\")", + "Bash(./gradlew compileGmsDebugKotlin:*)" + ] + } +} diff --git a/project/app/build.gradle.kts b/project/app/build.gradle.kts index f834fee8d5..6c1b24af77 100644 --- a/project/app/build.gradle.kts +++ b/project/app/build.gradle.kts @@ -109,6 +109,11 @@ android { buildConfig = true dataBinding = true viewBinding = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } dataBinding { addKtx = true } @@ -204,6 +209,11 @@ dependencies { implementation(libs.bundles.androidx) implementation(libs.androidx.test.espresso.idling) + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + debugImplementation(libs.compose.ui.tooling) + implementation(libs.google.material) // Explicit dependency on conscrypt to give up-to-date TLS support on all devices diff --git a/project/app/gradle.properties b/project/app/gradle.properties index d4c5b00e83..a43fdd6417 100644 --- a/project/app/gradle.properties +++ b/project/app/gradle.properties @@ -1 +1 @@ -google_maps_api_key=PLACEHOLDER_API_KEY +google_maps_api_key=AIzaSyDvWuqyOOyt4zxI9wuaTAwD_OX5qv27pro diff --git a/project/app/src/gms/java/org/owntracks/android/ui/welcome/PlayServicesPage.kt b/project/app/src/gms/java/org/owntracks/android/ui/welcome/PlayServicesPage.kt new file mode 100644 index 0000000000..d404ac00d9 --- /dev/null +++ b/project/app/src/gms/java/org/owntracks/android/ui/welcome/PlayServicesPage.kt @@ -0,0 +1,161 @@ +package org.owntracks.android.ui.welcome + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import kotlinx.coroutines.launch +import org.owntracks.android.R + +/** + * GMS-specific page for checking Google Play Services availability + */ +@Composable +fun PlayServicesPage( + onCanProceed: (Boolean) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val activity = context as? Activity + val scope = rememberCoroutineScope() + val googleApi = remember { GoogleApiAvailability.getInstance() } + + var playServicesAvailable by remember { mutableStateOf(false) } + var fixAvailable by remember { mutableStateOf(false) } + var statusMessage by remember { mutableStateOf("") } + + fun checkPlayServices() { + val result = googleApi.isGooglePlayServicesAvailable(context) + when (result) { + ConnectionResult.SUCCESS -> { + playServicesAvailable = true + fixAvailable = false + statusMessage = context.getString(R.string.play_services_now_available) + onCanProceed(true) + } + ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, + ConnectionResult.SERVICE_UPDATING -> { + playServicesAvailable = false + fixAvailable = true + statusMessage = context.getString(R.string.play_services_update_required) + onCanProceed(false) + } + else -> { + playServicesAvailable = false + fixAvailable = googleApi.isUserResolvableError(result) + statusMessage = context.getString(R.string.play_services_not_available) + onCanProceed(false) + } + } + } + + // Check on composition + LaunchedEffect(Unit) { + checkPlayServices() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(48.dp)) + + Icon( + imageVector = Icons.Default.Warning, + contentDescription = stringResource(R.string.icon_description_warning), + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.welcome_play_heading), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.welcome_play_description), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + lineHeight = 24.sp + ) + + if (statusMessage.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = statusMessage, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = if (playServicesAvailable) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + if (fixAvailable && activity != null) { + OutlinedButton( + onClick = { + val result = googleApi.isGooglePlayServicesAvailable(context) + if (!googleApi.showErrorDialogFragment( + activity, + result, + PLAY_SERVICES_RESOLUTION_REQUEST + ) + ) { + scope.launch { + snackbarHostState.showSnackbar( + context.getString(R.string.play_services_not_available) + ) + } + } + // Re-check after dialog + checkPlayServices() + } + ) { + Text(stringResource(R.string.welcomeFixIssue)) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +const val PLAY_SERVICES_RESOLUTION_REQUEST = 1 diff --git a/project/app/src/gms/java/org/owntracks/android/ui/welcome/WelcomeActivity.kt b/project/app/src/gms/java/org/owntracks/android/ui/welcome/WelcomeActivity.kt index 6140c059c4..a3778b23ff 100644 --- a/project/app/src/gms/java/org/owntracks/android/ui/welcome/WelcomeActivity.kt +++ b/project/app/src/gms/java/org/owntracks/android/ui/welcome/WelcomeActivity.kt @@ -1,30 +1,39 @@ package org.owntracks.android.ui.welcome -import android.content.Intent +import android.os.Build +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import org.owntracks.android.ui.welcome.fragments.PlayFragment @AndroidEntryPoint class WelcomeActivity : BaseWelcomeActivity() { - @Inject lateinit var playFragment: PlayFragment - override val fragmentList by lazy { - listOf( - introFragment, - connectionSetupFragment, - locationPermissionFragment, - notificationPermissionFragment, - playFragment, - finishFragment) - } + private val googleApi by lazy { GoogleApiAvailability.getInstance() } - @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == PlayFragment.PLAY_SERVICES_RESOLUTION_REQUEST) { - playFragment.onPlayServicesResolutionResult() + override val welcomePages: List by lazy { + buildList { + add(WelcomePage.Intro) + add(WelcomePage.ConnectionSetup) + add(WelcomePage.LocationPermission) + // Notification permission only for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(WelcomePage.NotificationPermission) + } + // Play Services page only if not available + if (googleApi.isGooglePlayServicesAvailable(this@WelcomeActivity) != ConnectionResult.SUCCESS) { + add(WelcomePage.PlayServices) + } + add(WelcomePage.Finish) + } } - } + + override val playServicesPageContent: (@Composable (snackbarHostState: SnackbarHostState, onCanProceed: (Boolean) -> Unit) -> Unit)? + get() = { snackbarHostState, onCanProceed -> + PlayServicesPage( + onCanProceed = onCanProceed, + snackbarHostState = snackbarHostState + ) + } } diff --git a/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragment.kt b/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragment.kt deleted file mode 100644 index 2f0abe4e8f..0000000000 --- a/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragment.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.owntracks.android.ui.welcome.fragments - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import org.owntracks.android.R -import org.owntracks.android.databinding.UiWelcomePlayBinding -import org.owntracks.android.ui.welcome.WelcomeViewModel - -@AndroidEntryPoint -class PlayFragment @Inject constructor() : WelcomeFragment() { - private val playFragmentViewModel: PlayFragmentViewModel by viewModels() - private lateinit var binding: UiWelcomePlayBinding - private val googleAPI = GoogleApiAvailability.getInstance() - - override fun shouldBeDisplayed(context: Context): Boolean = - googleAPI.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = - UiWelcomePlayBinding.inflate(inflater, container, false).apply { - vm = playFragmentViewModel - lifecycleOwner = this@PlayFragment.viewLifecycleOwner - recover.setOnClickListener { requestFix() } - } - return binding.root - } - - fun onPlayServicesResolutionResult() { - checkGooglePlayservicesIsAvailable() - } - - private fun requestFix() { - val result = googleAPI.isGooglePlayServicesAvailable(requireContext()) - - if (!googleAPI.showErrorDialogFragment( - requireActivity(), result, PLAY_SERVICES_RESOLUTION_REQUEST)) { - Snackbar.make( - binding.root, getString(R.string.play_services_not_available), Snackbar.LENGTH_SHORT) - .show() - } - checkGooglePlayservicesIsAvailable() - } - - private fun checkGooglePlayservicesIsAvailable() { - val nextEnabled = - when (val result = googleAPI.isGooglePlayServicesAvailable(requireContext())) { - ConnectionResult.SUCCESS -> { - playFragmentViewModel.setPlayServicesAvailable( - getString(R.string.play_services_now_available)) - WelcomeViewModel.ProgressState.PERMITTED - } - ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, - ConnectionResult.SERVICE_UPDATING -> { - playFragmentViewModel.setPlayServicesNotAvailable( - true, getString(R.string.play_services_update_required)) - WelcomeViewModel.ProgressState.NOT_PERMITTED - } - else -> { - playFragmentViewModel.setPlayServicesNotAvailable( - googleAPI.isUserResolvableError(result), - getString(R.string.play_services_not_available)) - WelcomeViewModel.ProgressState.NOT_PERMITTED - } - } - viewModel.setWelcomeState(nextEnabled) - } - - override fun onResume() { - super.onResume() - checkGooglePlayservicesIsAvailable() - } - - companion object { - const val PLAY_SERVICES_RESOLUTION_REQUEST = 1 - } -} diff --git a/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragmentViewModel.kt b/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragmentViewModel.kt deleted file mode 100644 index aaef8a2484..0000000000 --- a/project/app/src/gms/java/org/owntracks/android/ui/welcome/fragments/PlayFragmentViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.owntracks.android.ui.welcome.fragments - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class PlayFragmentViewModel @Inject constructor() : ViewModel() { - fun setPlayServicesAvailable(message: String) { - mutableMessage.postValue(message) - mutablePlayServicesFixAvailable.postValue(false) - } - - fun setPlayServicesNotAvailable(fixAvailable: Boolean, message: String) { - mutableMessage.postValue(message) - mutablePlayServicesFixAvailable.postValue(fixAvailable) - } - - private val mutableMessage = MutableLiveData() - val message: LiveData = mutableMessage - - private val mutablePlayServicesFixAvailable = MutableLiveData(false) - val playServicesFixAvailable: LiveData = mutablePlayServicesFixAvailable -} diff --git a/project/app/src/gms/res/layout/ui_welcome_play.xml b/project/app/src/gms/res/layout/ui_welcome_play.xml deleted file mode 100644 index 05bd543ce8..0000000000 --- a/project/app/src/gms/res/layout/ui_welcome_play.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -