From 9fa3549c5c831854a7bb7e4d33eac67f780c7c4f Mon Sep 17 00:00:00 2001 From: Maksim Zoteev <39910552+F0x1d@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:06:17 +0300 Subject: [PATCH] [feat]: update v2.0.2 * [feat]: disabling word wrap for log in details * [feat]: search in crash logs + small refactor * [feat]: moved apps chooser to another module and refactored to compose * [feat]: crashes blacklist * [build]: renamed modules * [fix]: opening crashes by default * [feat]: blacklisting in crash details * [feat]: search text in crash log * [feat]: disabling monet --- app/build.gradle.kts | 31 +-- .../main/kotlin/com/f0x1d/logfox/LogFoxApp.kt | 23 +- .../com/f0x1d/logfox/coil/AppIconFetcher.kt | 49 ++++ .../kotlin/com/f0x1d/logfox/di/CoilModule.kt | 27 ++ ...MainActivityPendingIntentProviderModule.kt | 2 +- .../settings/LoggingServiceDelegateModule.kt | 2 +- .../com/f0x1d/logfox/receiver/BootReceiver.kt | 4 +- .../f0x1d/logfox/ui/activity/MainActivity.kt | 49 ++-- .../f0x1d/logfox/viewmodel/MainViewModel.kt | 11 +- .../main/res/layout-land/activity_main.xml | 1 - .../res/layout-land/activity_main_no_bar.xml | 1 - app/src/main/res/layout/activity_main.xml | 1 - .../main/res/layout/activity_main_no_bar.xml | 1 - .../AndroidComposeConventionPlugin.kt | 2 + .../main/kotlin/extensions/Dependencies.kt | 21 +- core/{core-arch => arch}/.gitignore | 0 core/{core-arch => arch}/build.gradle.kts | 0 .../src/main/AndroidManifest.xml | 2 +- .../main/kotlin/com/f0x1d/logfox/arch/API.kt | 1 + .../com/f0x1d/logfox/arch}/ContextExt.kt | 3 +- .../kotlin/com/f0x1d/logfox/arch}/FileExt.kt | 2 +- .../com/f0x1d/logfox/arch}/NotificationExt.kt | 2 +- .../com/f0x1d/logfox/arch}/PendingIntents.kt | 3 +- .../logfox/arch/adapter/BaseListAdapter.kt | 0 .../f0x1d/logfox/arch/annotations/GsonSkip.kt | 0 .../f0x1d/logfox/arch/di/DispatchersModule.kt | 0 .../com/f0x1d/logfox/arch/di/GsonModule.kt | 0 .../com/f0x1d/logfox/arch/di/UtilsModule.kt | 0 .../f0x1d/logfox/arch}/io/OutputStreamExt.kt | 2 +- .../logfox/arch}/io/ZipOutputStreamExt.kt | 2 +- .../logfox/arch}/receiver/CopyReceiver.kt | 8 +- .../logfox/arch/repository/BaseRepository.kt | 0 .../repository/DatabaseProxyRepository.kt | 0 .../com/f0x1d/logfox/arch/ui/SnackbarExt.kt | 0 .../com/f0x1d/logfox/arch/ui/WindowExt.kt | 0 .../logfox/arch/ui/activity/BaseActivity.kt | 3 +- .../arch/ui/activity/BaseViewModelActivity.kt | 24 ++ .../arch/ui/base/SimpleLifecycleOwner.kt | 33 +++ .../logfox/arch/ui/dialog/BaseBottomSheet.kt | 18 +- .../ui/dialog/BaseViewModelBottomSheet.kt | 9 +- .../logfox/arch/ui/fragment/BaseFragment.kt | 18 +- .../arch/ui/fragment/BaseViewModelFragment.kt | 22 +- .../fragment/compose/BaseComposeFragment.kt | 18 +- .../compose/BaseComposeViewModelFragment.kt | 12 +- .../arch/ui/viewholder/BaseViewHolder.kt | 0 .../arch/viewmodel/BaseStateViewModel.kt | 1 + .../logfox/arch/viewmodel/BaseViewModel.kt | 54 ++++ .../com/f0x1d/logfox/arch/viewmodel/Event.kt | 7 + .../color/navbar_transparent_background.xml | 0 .../src/main/res/layout/fragment_compose.xml | 0 .../src/main/res/values/colors.xml | 0 .../logfox/arch/coroutines/flow/FlowExt.kt | 8 - .../arch/ui/activity/BaseViewModelActivity.kt | 31 --- .../logfox/arch/viewmodel/BaseViewModel.kt | 59 ---- core/core-context/build.gradle.kts | 10 - core/core-intents/build.gradle.kts | 9 - core/core-io/build.gradle.kts | 9 - .../ui/compose/component/button/RichButton.kt | 84 ------ core/{core-context => database}/.gitignore | 0 .../build.gradle.kts | 2 +- .../10.json | 0 .../11.json | 0 .../12.json | 0 .../13.json | 0 .../14.json | 0 .../15.json | 0 .../16.json | 0 .../17.json | 254 ++++++++++++++++++ .../7.json | 0 .../8.json | 0 .../9.json | 0 .../com/f0x1d/logfox/database/AppDatabase.kt | 10 +- .../f0x1d/logfox/database/di/RoomModule.kt | 0 .../f0x1d/logfox/database/entity/AppCrash.kt | 0 .../logfox/database/entity/DisabledApp.kt | 55 ++++ .../logfox/database/entity/LogRecording.kt | 0 .../logfox/database/entity/UserFilter.kt | 0 core/{core-database => datetime}/.gitignore | 0 .../build.gradle.kts | 2 +- .../com/f0x1d/logfox/datetime/ContextExt.kt | 0 .../logfox/datetime/DateTimeFormatter.kt | 17 +- .../datetime/DateTimeFormatterModule.kt | 16 ++ core/{core-datetime => navigation}/.gitignore | 0 .../build.gradle.kts | 0 .../com/f0x1d/logfox/navigation/Navigation.kt | 0 .../src/main/res/navigation/crashes.xml | 12 + .../src/main/res/navigation/logs.xml | 10 +- .../src/main/res/navigation/nav_graph.xml | 0 .../src/main/res/navigation/recordings.xml | 0 .../src/main/res/navigation/settings.xml | 0 core/{core-intents => preferences}/.gitignore | 0 .../build.gradle.kts | 2 +- .../preferences/shared/AppPreferences.kt | 7 + .../logfox/preferences/shared/ContextExt.kt | 0 .../shared/base/BasePreferences.kt | 0 .../preferences/shared/crashes/CrashesSort.kt | 0 core/{core-io => terminals}/.gitignore | 0 .../build.gradle.kts | 2 +- .../src/main/AndroidManifest.xml | 0 .../aidl/com/f0x1d/logfox/IUserService.aidl | 0 .../logfox/model/terminal/TerminalResult.aidl | 0 .../com/f0x1d/logfox/di/TerminalsModule.kt | 0 .../com/f0x1d/logfox/service/UserService.kt | 0 .../f0x1d/logfox/terminals/DefaultTerminal.kt | 0 .../f0x1d/logfox/terminals/RootTerminal.kt | 0 .../f0x1d/logfox/terminals/ShizukuTerminal.kt | 0 .../f0x1d/logfox/terminals/base/Terminal.kt | 0 .../src/main/res/values-it/strings.xml | 0 .../src/main/res/values-pt-rBR/strings.xml | 0 .../src/main/res/values-ru/strings.xml | 0 .../src/main/res/values-tr/strings.xml | 0 .../src/main/res/values-zh-rCN/strings.xml | 0 .../src/main/res/values/strings.xml | 0 core/{core-navigation => tests}/.gitignore | 0 core/{core-tests => tests}/build.gradle.kts | 0 .../f0x1d/logfox/core/tests/ScreenshotTest.kt | 2 +- .../SemanticsNodeInteractionsProviderExt.kt | 0 .../.gitignore | 0 .../build.gradle.kts | 2 +- .../component/button/NavigationBackButton.kt | 32 +++ .../ui/compose/component/button/RichButton.kt | 54 ++++ .../compose/component/search/TopSearchBar.kt | 65 +++++ .../ui/compose/preview/DayNightPreview.kt | 0 .../f0x1d/logfox/ui/compose/theme/Color.kt | 0 .../f0x1d/logfox/ui/compose/theme/Theme.kt | 0 .../com/f0x1d/logfox/ui/compose/theme/Type.kt | 0 .../src/main/res/values/font_certs.xml | 0 core/{core-ui => ui}/.gitignore | 0 core/{core-ui => ui}/build.gradle.kts | 4 +- .../main/kotlin/com/f0x1d/logfox/ui/Colors.kt | 3 + .../main/kotlin/com/f0x1d/logfox/ui/Icons.kt | 0 .../com/f0x1d/logfox/ui/density/PxExt.kt | 0 .../com/f0x1d/logfox/ui/di/ViewPumpModule.kt | 0 .../logfox/ui/dialog/AreYouSureDialogExt.kt | 0 .../logfox/ui/glide/icon/IconDataFetcher.kt | 0 .../logfox/ui/glide/icon/IconGlideModule.kt | 0 .../logfox/ui/glide/icon/IconModelLoader.kt | 0 .../ui/glide/icon/IconModelLoaderFactory.kt | 0 .../logfox/ui/interceptor/FontsInterceptor.kt | 0 .../CustomApplyInsetsNavigationRailView.kt | 0 .../logfox/ui/view/CustomNestedScrollView.kt | 0 .../kotlin/com/f0x1d/logfox/ui/view/FABExt.kt | 0 .../com/f0x1d/logfox/ui/view/ImageViewExt.kt | 0 .../com/f0x1d/logfox/ui/view/MenuExt.kt | 0 .../ui/view/OnlyUserCheckedChangeListener.kt | 0 .../com/f0x1d/logfox/ui/view/PreferenceExt.kt | 2 +- .../com/f0x1d/logfox/ui/view/ToolbarExt.kt | 0 .../ui/view/loglevel/LogLevelExtensions.kt | 0 .../logfox/ui/view/loglevel/LogLevelView.kt | 0 .../res/color/item_log_background_ripple.xml | 0 .../item_log_level_background.xml | 0 .../src/main/res/drawable/ic_adb.xml | 0 .../src/main/res/drawable/ic_add.xml | 0 .../src/main/res/drawable/ic_add_link.xml | 0 .../src/main/res/drawable/ic_alert.xml | 0 .../src/main/res/drawable/ic_android.xml | 0 .../src/main/res/drawable/ic_android_anim.xml | 0 .../src/main/res/drawable/ic_android_avd.xml | 0 .../src/main/res/drawable/ic_archive.xml | 0 .../src/main/res/drawable/ic_arrow_back.xml | 0 .../main/res/drawable/ic_arrow_drop_down.xml | 0 core/ui/src/main/res/drawable/ic_block.xml | 12 + .../src/main/res/drawable/ic_bug.xml | 0 .../src/main/res/drawable/ic_bug_anim.xml | 0 .../src/main/res/drawable/ic_bug_avd.xml | 0 .../main/res/drawable/ic_bug_notification.xml | 0 .../src/main/res/drawable/ic_check_circle.xml | 10 + .../src/main/res/drawable/ic_checklist.xml | 0 .../src/main/res/drawable/ic_clear.xml | 0 .../src/main/res/drawable/ic_clear_all.xml | 0 .../src/main/res/drawable/ic_copy.xml | 0 .../src/main/res/drawable/ic_delete.xml | 0 .../src/main/res/drawable/ic_dialog_adb.xml | 0 .../res/drawable/ic_dialog_date_format.xml | 0 .../src/main/res/drawable/ic_dialog_eye.xml | 0 .../src/main/res/drawable/ic_dialog_list.xml | 0 .../ic_dialog_notification_important.xml | 0 .../main/res/drawable/ic_dialog_terminal.xml | 0 .../res/drawable/ic_dialog_text_fields.xml | 0 .../src/main/res/drawable/ic_dialog_theme.xml | 0 .../res/drawable/ic_dialog_time_format.xml | 0 .../src/main/res/drawable/ic_dialog_timer.xml | 0 .../main/res/drawable/ic_dialog_warning.xml | 0 .../src/main/res/drawable/ic_export.xml | 0 .../src/main/res/drawable/ic_eye.xml | 0 .../src/main/res/drawable/ic_filter.xml | 0 .../src/main/res/drawable/ic_info.xml | 0 .../res/drawable/ic_launcher_foreground.xml | 0 .../src/main/res/drawable/ic_logfox.xml | 0 .../src/main/res/drawable/ic_logfox_anim.xml | 0 .../src/main/res/drawable/ic_logfox_avd.xml | 0 .../main/res/drawable/ic_notifications.xml | 0 .../src/main/res/drawable/ic_pause.xml | 0 .../src/main/res/drawable/ic_play.xml | 0 .../src/main/res/drawable/ic_recording.xml | 0 .../main/res/drawable/ic_recording_anim.xml | 0 .../main/res/drawable/ic_recording_avd.xml | 0 .../drawable/ic_recording_notification.xml | 0 .../ic_recording_play_notification.xml | 0 .../src/main/res/drawable/ic_save.xml | 0 .../src/main/res/drawable/ic_search.xml | 3 +- .../src/main/res/drawable/ic_select.xml | 0 .../src/main/res/drawable/ic_select_all.xml | 0 .../src/main/res/drawable/ic_settings.xml | 0 .../main/res/drawable/ic_settings_anim.xml | 0 .../src/main/res/drawable/ic_settings_avd.xml | 0 .../main/res/drawable/ic_settings_code.xml | 0 .../main/res/drawable/ic_settings_crashes.xml | 0 .../res/drawable/ic_settings_handyman.xml | 0 .../main/res/drawable/ic_settings_info.xml | 0 .../drawable/ic_settings_notifications.xml | 0 .../main/res/drawable/ic_settings_person.xml | 0 .../res/drawable/ic_settings_releases.xml | 0 .../main/res/drawable/ic_settings_service.xml | 0 .../src/main/res/drawable/ic_settings_ui.xml | 0 .../main/res/drawable/ic_settings_users.xml | 0 .../main/res/drawable/ic_settings_warning.xml | 0 .../src/main/res/drawable/ic_share.xml | 0 .../src/main/res/drawable/ic_sort.xml | 0 .../src/main/res/drawable/ic_square_root.xml | 0 .../src/main/res/drawable/ic_stop.xml | 0 .../src/main/res/drawable/ic_terminal.xml | 0 .../main/res/drawable/item_log_background.xml | 0 .../drawable/item_log_level_background.xml | 0 .../drawable/placeholder_icon_background.xml | 0 .../src/main/res/font/google_sans.ttf | Bin .../src/main/res/font/google_sans_medium.ttf | Bin .../src/main/res/layout/dialog_text.xml | 0 .../src/main/res/layout/fragment_settings.xml | 0 .../src/main/res/values-night/colors.xml | 0 .../src/main/res/values/colors.xml | 0 .../src/main/res/values/ids.xml | 0 .../src/main/res/values/styles.xml | 1 + .../src/main/res/values/themes.xml | 0 .../com/f0x1d/logfox/model/InstalledApp.kt | 2 +- .../com/f0x1d/logfox/model/event/Event.kt | 17 -- .../f0x1d/logfox/model/event/NoDataEvent.kt | 10 - .../f0x1d/logfox/model/event/SnackbarEvent.kt | 7 - .../.gitignore | 0 feature/apps-picker/build.gradle.kts | 9 + .../ui/fragment/picker/AppsPickerFragment.kt | 68 +++++ .../picker/compose/AppsPickerScreenContent.kt | 250 +++++++++++++++++ .../picker/compose/AppsPickerScreenState.kt | 34 +++ .../viewmodel/AppsPickerResultHandler.kt | 34 +++ .../picker/viewmodel/AppsPickerViewModel.kt | 80 ++++++ .../crashes-core}/.gitignore | 0 .../build.gradle.kts | 0 .../core/controller/CrashesController.kt | 42 +-- .../CrashesNotificationsController.kt | 14 +- .../crashes/core/di/ControllersModule.kt | 0 .../crashes/core/di/RepositoriesModule.kt | 7 + .../core/repository/CrashesRepository.kt | 25 +- .../core/repository/DisabledAppsRepository.kt | 80 ++++++ .../core/repository/reader/ANRDetector.kt | 0 .../repository/reader/JNICrashDetector.kt | 0 .../repository/reader/JavaCrashDetector.kt | 2 +- .../reader/base/BaseCrashDetector.kt | 0 .../repository/reader/base/DefaultChecker.kt | 0 .../{feature-logging => crashes}/.gitignore | 0 .../build.gradle.kts | 4 +- .../feature/crashes/adapter/CrashesAdapter.kt | 0 .../crashes/di/AppCrashesViewModelModule.kt | 0 .../crashes/di/CrashDetailsViewModelModule.kt | 0 .../ui/fragment/CrashDetailsFragment.kt | 217 +++++++++++++++ .../ui/fragment/list/AppCrashesFragment.kt | 0 .../ui/fragment/list/CrashesFragment.kt | 9 +- .../crashes/ui/viewholder/CrashViewHolder.kt | 0 .../viewmodel/CrashDetailsViewModel.kt | 35 ++- .../viewmodel/list/AppCrashesViewModel.kt | 0 .../viewmodel/list/CrashesViewModel.kt | 69 +++-- .../src/main/res/layout/dialog_sorting.xml | 0 .../main/res/layout/fragment_app_crashes.xml | 0 .../res/layout/fragment_crash_details.xml | 27 +- .../src/main/res/layout/fragment_crashes.xml | 0 .../src/main/res/layout/item_crash.xml | 0 .../src/main/res/layout/item_sort.xml | 0 .../main/res/layout/placeholder_crashes.xml | 0 .../src/main/res/menu/crash_details_menu.xml | 13 + .../src/main/res/menu/crashes_menu.xml | 6 + .../ui/fragment/CrashDetailsFragment.kt | 112 -------- .../feature/filters/adapter/AppsAdapter.kt | 19 -- .../filters/ui/fragment/ChooseAppFragment.kt | 110 -------- .../filters/ui/viewholder/AppViewHolder.kt | 30 --- .../filters/viewmodel/ChooseAppViewModel.kt | 64 ----- .../main/res/layout/fragment_choose_app.xml | 48 ---- .../src/main/res/layout/item_app.xml | 51 ---- .../src/main/res/menu/choose_app_menu.xml | 8 - feature/feature-logging/build.gradle.kts | 12 - feature/feature-recordings/.gitignore | 1 - feature/feature-settings/.gitignore | 1 - feature/feature-setup/.gitignore | 1 - ...tTest.shouldShowDarkSetupScreenContent.png | Bin 39741 -> 0 bytes .../filters-core}/.gitignore | 0 .../build.gradle.kts | 0 .../filters/core/di/RepositoriesModule.kt | 0 .../core/repository/FiltersRepository.kt | 4 +- .../filters}/.gitignore | 0 .../build.gradle.kts | 6 +- .../feature/filters/adapter/FiltersAdapter.kt | 0 .../filters/di/EditFilterViewModelModule.kt | 0 .../filters/ui/fragment/EditFilterFragment.kt | 20 +- .../filters/ui/fragment/FiltersFragment.kt | 0 .../filters/ui/viewholder/FilterViewHolder.kt | 0 .../filters/viewmodel/EditFilterViewModel.kt | 21 +- .../filters/viewmodel/FiltersViewModel.kt | 0 .../main/res/layout/fragment_edit_filter.xml | 0 .../src/main/res/layout/fragment_filters.xml | 0 .../src/main/res/layout/item_filter.xml | 0 .../main/res/layout/placeholder_filters.xml | 0 .../src/main/res/menu/edit_filter_menu.xml | 0 .../src/main/res/menu/filters_menu.xml | 0 .../.gitignore | 0 .../build.gradle.kts | 0 .../logging/core/di/RepositoriesModule.kt | 0 .../feature/logging/core/di/StoresModule.kt | 0 .../feature/logging/core/model/LogLinesExt.kt | 0 .../core/repository/LoggingRepository.kt | 0 .../logging/core/store/LoggingStore.kt | 0 feature/logging/.gitignore | 1 + feature/logging/build.gradle.kts | 12 + .../src/main/AndroidManifest.xml | 0 .../feature/logging/adapter/LogsAdapter.kt | 12 +- .../feature/logging/di/LogsViewModelModule.kt | 0 .../feature/logging/service/LoggingService.kt | 12 +- .../MainActivityPendingIntentProvider.kt | 0 .../logging/ui/dialog/SearchBottomSheet.kt | 0 .../ui/fragment/LogsExtendedCopyFragment.kt | 0 .../logging/ui/fragment/LogsFragment.kt | 18 +- .../logging/ui/viewholder/LogViewHolder.kt | 0 .../logging/viewmodel/LogsViewModel.kt | 23 +- .../f0x1d/feature/logging/viewmodel/UriExt.kt | 0 .../src/main/res/layout/fragment_logs.xml | 0 .../layout/fragment_logs_extended_copy.xml | 0 .../src/main/res/layout/item_log.xml | 0 .../src/main/res/layout/placeholder_logs.xml | 0 .../src/main/res/layout/sheet_search.xml | 0 .../src/main/res/menu/log_menu.xml | 0 .../src/main/res/menu/logs_menu.xml | 0 .../.gitignore | 0 .../build.gradle.kts | 2 +- .../src/main/AndroidManifest.xml | 0 .../core/controller/RecordingController.kt | 0 .../RecordingNotificationController.kt | 14 +- .../core/controller/reader/RecordingReader.kt | 0 .../recordings/core/di/ControllersModule.kt | 0 .../recordings/core/di/RepositoriesModule.kt | 0 .../core/receiver/RecordingReceiver.kt | 0 .../core/repository/RecordingsRepository.kt | 7 +- .../.gitignore | 0 .../build.gradle.kts | 2 +- .../recordings/adapter/RecordingsAdapter.kt | 0 .../recordings/di/RecordingViewModelModule.kt | 0 .../ui/dialog/RecordingBottomSheet.kt | 8 +- .../ui/fragment/RecordingsFragment.kt | 11 +- .../ui/viewholder/RecordingViewHolder.kt | 0 .../viewmodel/RecordingViewModel.kt | 8 +- .../viewmodel/RecordingsViewModel.kt | 13 +- .../main/res/layout/fragment_recordings.xml | 0 .../src/main/res/layout/item_recording.xml | 0 .../res/layout/placeholder_recordings.xml | 0 .../src/main/res/layout/sheet_recording.xml | 0 .../src/main/res/menu/recordings_menu.xml | 0 .../.gitignore | 0 .../build.gradle.kts | 0 .../logfox/feature/settings/IntArrayExt.kt | 0 .../settings/LoggingServiceDelegate.kt | 0 .../ui/fragment/SettingsCrashesFragment.kt | 0 .../ui/fragment/SettingsLinksFragment.kt | 0 .../ui/fragment/SettingsMenuFragment.kt | 0 .../fragment/SettingsNotificationsFragment.kt | 4 +- .../ui/fragment/SettingsServiceFragment.kt | 2 +- .../ui/fragment/SettingsUIFragment.kt | 11 +- .../fragment/base/BasePreferenceFragment.kt | 20 +- .../res/layout/preference_material_switch.xml | 0 .../main/res/layout/preference_warning.xml | 0 .../src/main/res/values/ids.xml | 0 .../src/main/res/xml/settings_crashes.xml | 0 .../src/main/res/xml/settings_links.xml | 0 .../src/main/res/xml/settings_menu.xml | 0 .../main/res/xml/settings_notifications.xml | 0 .../src/main/res/xml/settings_service.xml | 0 .../src/main/res/xml/settings_ui.xml | 15 ++ .../.gitignore | 0 .../{feature-setup => setup}/build.gradle.kts | 0 .../setup/ui/fragment/setup/SetupFragment.kt | 0 .../setup/compose/SetupScreenContent.kt | 27 +- .../setup/compose/SetupScreenState.kt | 0 .../feature/setup/viewmodel/SetupViewModel.kt | 6 +- .../setup/compose/SetupScreenContentTest.kt | 0 ...houldOpenAdbDialogOnSetupScreenContent.png | Bin ...houldShowAdbDialogOnSetupScreenContent.png | Bin ...tTest.shouldShowDarkSetupScreenContent.png | Bin 0 -> 40052 bytes ...ntentTest.shouldShowSetupScreenContent.png | Bin 81418 -> 81418 bytes gradle/libs.versions.toml | 18 +- settings.gradle.kts | 40 ++- strings/src/main/res/values-ru/strings.xml | 4 + strings/src/main/res/values/strings.xml | 6 + 397 files changed, 2038 insertions(+), 1085 deletions(-) create mode 100644 app/src/main/kotlin/com/f0x1d/logfox/coil/AppIconFetcher.kt create mode 100644 app/src/main/kotlin/com/f0x1d/logfox/di/CoilModule.kt rename core/{core-arch => arch}/.gitignore (100%) rename core/{core-arch => arch}/build.gradle.kts (100%) rename core/{core-context => arch}/src/main/AndroidManifest.xml (66%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/API.kt (92%) rename core/{core-context/src/main/kotlin/com/f0x1d/logfox/context => arch/src/main/kotlin/com/f0x1d/logfox/arch}/ContextExt.kt (96%) rename core/{core-context/src/main/kotlin/com/f0x1d/logfox/context => arch/src/main/kotlin/com/f0x1d/logfox/arch}/FileExt.kt (88%) rename core/{core-context/src/main/kotlin/com/f0x1d/logfox/context => arch/src/main/kotlin/com/f0x1d/logfox/arch}/NotificationExt.kt (81%) rename core/{core-intents/src/main/kotlin/com/f0x1d/logfox/intents => arch/src/main/kotlin/com/f0x1d/logfox/arch}/PendingIntents.kt (95%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/annotations/GsonSkip.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/di/DispatchersModule.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/di/GsonModule.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/di/UtilsModule.kt (100%) rename core/{core-io/src/main/kotlin/com/f0x1d/logfox => arch/src/main/kotlin/com/f0x1d/logfox/arch}/io/OutputStreamExt.kt (87%) rename core/{core-io/src/main/kotlin/com/f0x1d/logfox => arch/src/main/kotlin/com/f0x1d/logfox/arch}/io/ZipOutputStreamExt.kt (93%) rename core/{core-context/src/main/kotlin/com/f0x1d/logfox/context => arch/src/main/kotlin/com/f0x1d/logfox/arch}/receiver/CopyReceiver.kt (79%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/repository/DatabaseProxyRepository.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt (96%) create mode 100644 core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt create mode 100644 core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt (78%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt (73%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt (73%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt (51%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt (73%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt (74%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt (100%) rename core/{core-arch => arch}/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt (93%) create mode 100644 core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt create mode 100644 core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt rename core/{core-arch => arch}/src/main/res/color/navbar_transparent_background.xml (100%) rename core/{core-arch => arch}/src/main/res/layout/fragment_compose.xml (100%) rename core/{core-arch => arch}/src/main/res/values/colors.xml (100%) delete mode 100644 core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/coroutines/flow/FlowExt.kt delete mode 100644 core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt delete mode 100644 core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt delete mode 100644 core/core-context/build.gradle.kts delete mode 100644 core/core-intents/build.gradle.kts delete mode 100644 core/core-io/build.gradle.kts delete mode 100644 core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt rename core/{core-context => database}/.gitignore (100%) rename core/{core-database => database}/build.gradle.kts (89%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/10.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/11.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/12.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/13.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/14.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/15.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/16.json (100%) create mode 100644 core/database/schemas/com.f0x1d.logfox.database.AppDatabase/17.json rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/7.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/8.json (100%) rename core/{core-database => database}/schemas/com.f0x1d.logfox.database.AppDatabase/9.json (100%) rename core/{core-database => database}/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt (92%) rename core/{core-database => database}/src/main/kotlin/com/f0x1d/logfox/database/di/RoomModule.kt (100%) rename core/{core-database => database}/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt (100%) create mode 100644 core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/DisabledApp.kt rename core/{core-database => database}/src/main/kotlin/com/f0x1d/logfox/database/entity/LogRecording.kt (100%) rename core/{core-database => database}/src/main/kotlin/com/f0x1d/logfox/database/entity/UserFilter.kt (100%) rename core/{core-database => datetime}/.gitignore (100%) rename core/{core-datetime => datetime}/build.gradle.kts (72%) rename core/{core-datetime => datetime}/src/main/kotlin/com/f0x1d/logfox/datetime/ContextExt.kt (100%) rename core/{core-datetime => datetime}/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt (71%) create mode 100644 core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatterModule.kt rename core/{core-datetime => navigation}/.gitignore (100%) rename core/{core-navigation => navigation}/build.gradle.kts (100%) rename core/{core-navigation => navigation}/src/main/kotlin/com/f0x1d/logfox/navigation/Navigation.kt (100%) rename core/{core-navigation => navigation}/src/main/res/navigation/crashes.xml (80%) rename core/{core-navigation => navigation}/src/main/res/navigation/logs.xml (90%) rename core/{core-navigation => navigation}/src/main/res/navigation/nav_graph.xml (100%) rename core/{core-navigation => navigation}/src/main/res/navigation/recordings.xml (100%) rename core/{core-navigation => navigation}/src/main/res/navigation/settings.xml (100%) rename core/{core-intents => preferences}/.gitignore (100%) rename core/{core-preferences => preferences}/build.gradle.kts (78%) rename core/{core-preferences => preferences}/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt (96%) rename core/{core-preferences => preferences}/src/main/kotlin/com/f0x1d/logfox/preferences/shared/ContextExt.kt (100%) rename core/{core-preferences => preferences}/src/main/kotlin/com/f0x1d/logfox/preferences/shared/base/BasePreferences.kt (100%) rename core/{core-preferences => preferences}/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt (100%) rename core/{core-io => terminals}/.gitignore (100%) rename core/{core-terminals => terminals}/build.gradle.kts (84%) rename core/{core-terminals => terminals}/src/main/AndroidManifest.xml (100%) rename core/{core-terminals => terminals}/src/main/aidl/com/f0x1d/logfox/IUserService.aidl (100%) rename core/{core-terminals => terminals}/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/di/TerminalsModule.kt (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt (100%) rename core/{core-terminals => terminals}/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt (100%) rename core/{core-terminals => terminals}/src/main/res/values-it/strings.xml (100%) rename core/{core-terminals => terminals}/src/main/res/values-pt-rBR/strings.xml (100%) rename core/{core-terminals => terminals}/src/main/res/values-ru/strings.xml (100%) rename core/{core-terminals => terminals}/src/main/res/values-tr/strings.xml (100%) rename core/{core-terminals => terminals}/src/main/res/values-zh-rCN/strings.xml (100%) rename core/{core-terminals => terminals}/src/main/res/values/strings.xml (100%) rename core/{core-navigation => tests}/.gitignore (100%) rename core/{core-tests => tests}/build.gradle.kts (100%) rename core/{core-tests => tests}/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt (97%) rename core/{core-tests => tests}/src/main/kotlin/com/f0x1d/logfox/core/tests/compose/SemanticsNodeInteractionsProviderExt.kt (100%) rename core/{core-preferences => ui-compose}/.gitignore (100%) rename core/{core-ui-compose => ui-compose}/build.gradle.kts (76%) create mode 100644 core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/NavigationBackButton.kt create mode 100644 core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt create mode 100644 core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/search/TopSearchBar.kt rename core/{core-ui-compose => ui-compose}/src/main/kotlin/com/f0x1d/logfox/ui/compose/preview/DayNightPreview.kt (100%) rename core/{core-ui-compose => ui-compose}/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Color.kt (100%) rename core/{core-ui-compose => ui-compose}/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Theme.kt (100%) rename core/{core-ui-compose => ui-compose}/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Type.kt (100%) rename core/{core-ui-compose => ui-compose}/src/main/res/values/font_certs.xml (100%) rename core/{core-ui => ui}/.gitignore (100%) rename core/{core-ui => ui}/build.gradle.kts (71%) create mode 100644 core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Colors.kt rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/Icons.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/density/PxExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/di/ViewPumpModule.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/dialog/AreYouSureDialogExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconDataFetcher.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconGlideModule.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoader.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoaderFactory.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/interceptor/FontsInterceptor.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomApplyInsetsNavigationRailView.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomNestedScrollView.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/FABExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/ImageViewExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/MenuExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/OnlyUserCheckedChangeListener.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt (97%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/ToolbarExt.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelExtensions.kt (100%) rename core/{core-ui => ui}/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelView.kt (100%) rename core/{core-ui => ui}/src/main/res/color/item_log_background_ripple.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable-ldrtl/item_log_level_background.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_adb.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_add.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_add_link.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_alert.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_android.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_android_anim.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_android_avd.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_archive.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_arrow_back.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_arrow_drop_down.xml (100%) create mode 100644 core/ui/src/main/res/drawable/ic_block.xml rename core/{core-ui => ui}/src/main/res/drawable/ic_bug.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_bug_anim.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_bug_avd.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_bug_notification.xml (100%) create mode 100644 core/ui/src/main/res/drawable/ic_check_circle.xml rename core/{core-ui => ui}/src/main/res/drawable/ic_checklist.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_clear.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_clear_all.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_copy.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_delete.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_adb.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_date_format.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_eye.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_list.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_notification_important.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_terminal.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_text_fields.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_theme.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_time_format.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_timer.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_dialog_warning.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_export.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_eye.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_filter.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_info.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_launcher_foreground.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_logfox.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_logfox_anim.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_logfox_avd.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_notifications.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_pause.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_play.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_recording.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_recording_anim.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_recording_avd.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_recording_notification.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_recording_play_notification.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_save.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_search.xml (90%) rename core/{core-ui => ui}/src/main/res/drawable/ic_select.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_select_all.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_anim.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_avd.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_code.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_crashes.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_handyman.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_info.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_notifications.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_person.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_releases.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_service.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_ui.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_users.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_settings_warning.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_share.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_sort.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_square_root.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_stop.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/ic_terminal.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/item_log_background.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/item_log_level_background.xml (100%) rename core/{core-ui => ui}/src/main/res/drawable/placeholder_icon_background.xml (100%) rename core/{core-ui => ui}/src/main/res/font/google_sans.ttf (100%) rename core/{core-ui => ui}/src/main/res/font/google_sans_medium.ttf (100%) rename core/{core-ui => ui}/src/main/res/layout/dialog_text.xml (100%) rename core/{core-ui => ui}/src/main/res/layout/fragment_settings.xml (100%) rename core/{core-ui => ui}/src/main/res/values-night/colors.xml (100%) rename core/{core-ui => ui}/src/main/res/values/colors.xml (100%) rename core/{core-ui => ui}/src/main/res/values/ids.xml (100%) rename core/{core-ui => ui}/src/main/res/values/styles.xml (98%) rename core/{core-ui => ui}/src/main/res/values/themes.xml (100%) delete mode 100644 data/src/main/kotlin/com/f0x1d/logfox/model/event/Event.kt delete mode 100644 data/src/main/kotlin/com/f0x1d/logfox/model/event/NoDataEvent.kt delete mode 100644 data/src/main/kotlin/com/f0x1d/logfox/model/event/SnackbarEvent.kt rename feature/{feature-crashes => apps-picker}/.gitignore (100%) create mode 100644 feature/apps-picker/build.gradle.kts create mode 100644 feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt create mode 100644 feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt create mode 100644 feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt create mode 100644 feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt create mode 100644 feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt rename {core/core-terminals => feature/crashes-core}/.gitignore (100%) rename feature/{feature-crashes-core => crashes-core}/build.gradle.kts (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt (60%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt (93%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt (63%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt (75%) create mode 100644 feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt (100%) rename feature/{feature-crashes-core => crashes-core}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt (100%) rename feature/{feature-logging => crashes}/.gitignore (100%) rename feature/{feature-crashes => crashes}/build.gradle.kts (60%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt (100%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt (100%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt (100%) create mode 100644 feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt (100%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt (94%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt (100%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt (65%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt (100%) rename feature/{feature-crashes => crashes}/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt (64%) rename feature/{feature-crashes => crashes}/src/main/res/layout/dialog_sorting.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/layout/fragment_app_crashes.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/layout/fragment_crash_details.xml (90%) rename feature/{feature-crashes => crashes}/src/main/res/layout/fragment_crashes.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/layout/item_crash.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/layout/item_sort.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/layout/placeholder_crashes.xml (100%) rename feature/{feature-crashes => crashes}/src/main/res/menu/crash_details_menu.xml (61%) rename feature/{feature-crashes => crashes}/src/main/res/menu/crashes_menu.xml (74%) delete mode 100644 feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt delete mode 100644 feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/AppsAdapter.kt delete mode 100644 feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt delete mode 100644 feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/AppViewHolder.kt delete mode 100644 feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt delete mode 100644 feature/feature-filters/src/main/res/layout/fragment_choose_app.xml delete mode 100644 feature/feature-filters/src/main/res/layout/item_app.xml delete mode 100644 feature/feature-filters/src/main/res/menu/choose_app_menu.xml delete mode 100644 feature/feature-logging/build.gradle.kts delete mode 100644 feature/feature-recordings/.gitignore delete mode 100644 feature/feature-settings/.gitignore delete mode 100644 feature/feature-setup/.gitignore delete mode 100644 feature/feature-setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png rename {core/core-tests => feature/filters-core}/.gitignore (100%) rename feature/{feature-filters-core => filters-core}/build.gradle.kts (100%) rename feature/{feature-filters-core => filters-core}/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/di/RepositoriesModule.kt (100%) rename feature/{feature-filters-core => filters-core}/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/repository/FiltersRepository.kt (97%) rename {core/core-ui-compose => feature/filters}/.gitignore (100%) rename feature/{feature-filters => filters}/build.gradle.kts (55%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt (100%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt (100%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt (93%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt (100%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt (100%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt (89%) rename feature/{feature-filters => filters}/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt (100%) rename feature/{feature-filters => filters}/src/main/res/layout/fragment_edit_filter.xml (100%) rename feature/{feature-filters => filters}/src/main/res/layout/fragment_filters.xml (100%) rename feature/{feature-filters => filters}/src/main/res/layout/item_filter.xml (100%) rename feature/{feature-filters => filters}/src/main/res/layout/placeholder_filters.xml (100%) rename feature/{feature-filters => filters}/src/main/res/menu/edit_filter_menu.xml (100%) rename feature/{feature-filters => filters}/src/main/res/menu/filters_menu.xml (100%) rename feature/{feature-crashes-core => logging-core}/.gitignore (100%) rename feature/{feature-logging-core => logging-core}/build.gradle.kts (100%) rename feature/{feature-logging-core => logging-core}/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt (100%) rename feature/{feature-logging-core => logging-core}/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt (100%) rename feature/{feature-logging-core => logging-core}/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt (100%) rename feature/{feature-logging-core => logging-core}/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt (100%) rename feature/{feature-logging-core => logging-core}/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt (100%) create mode 100644 feature/logging/.gitignore create mode 100644 feature/logging/build.gradle.kts rename feature/{feature-logging => logging}/src/main/AndroidManifest.xml (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt (74%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt (96%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt (94%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt (100%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt (88%) rename feature/{feature-logging => logging}/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt (100%) rename feature/{feature-logging => logging}/src/main/res/layout/fragment_logs.xml (100%) rename feature/{feature-logging => logging}/src/main/res/layout/fragment_logs_extended_copy.xml (100%) rename feature/{feature-logging => logging}/src/main/res/layout/item_log.xml (100%) rename feature/{feature-logging => logging}/src/main/res/layout/placeholder_logs.xml (100%) rename feature/{feature-logging => logging}/src/main/res/layout/sheet_search.xml (100%) rename feature/{feature-logging => logging}/src/main/res/menu/log_menu.xml (100%) rename feature/{feature-logging => logging}/src/main/res/menu/logs_menu.xml (100%) rename feature/{feature-filters-core => recordings-core}/.gitignore (100%) rename feature/{feature-recordings-core => recordings-core}/build.gradle.kts (67%) rename feature/{feature-recordings-core => recordings-core}/src/main/AndroidManifest.xml (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt (90%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt (100%) rename feature/{feature-recordings-core => recordings-core}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt (96%) rename feature/{feature-filters => recordings}/.gitignore (100%) rename feature/{feature-recordings => recordings}/build.gradle.kts (65%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt (100%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt (100%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt (89%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/fragment/RecordingsFragment.kt (95%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt (100%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt (93%) rename feature/{feature-recordings => recordings}/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt (88%) rename feature/{feature-recordings => recordings}/src/main/res/layout/fragment_recordings.xml (100%) rename feature/{feature-recordings => recordings}/src/main/res/layout/item_recording.xml (100%) rename feature/{feature-recordings => recordings}/src/main/res/layout/placeholder_recordings.xml (100%) rename feature/{feature-recordings => recordings}/src/main/res/layout/sheet_recording.xml (100%) rename feature/{feature-recordings => recordings}/src/main/res/menu/recordings_menu.xml (100%) rename feature/{feature-logging-core => settings}/.gitignore (100%) rename feature/{feature-settings => settings}/build.gradle.kts (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt (100%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt (95%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt (99%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt (95%) rename feature/{feature-settings => settings}/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt (67%) rename feature/{feature-settings => settings}/src/main/res/layout/preference_material_switch.xml (100%) rename feature/{feature-settings => settings}/src/main/res/layout/preference_warning.xml (100%) rename feature/{feature-settings => settings}/src/main/res/values/ids.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_crashes.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_links.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_menu.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_notifications.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_service.xml (100%) rename feature/{feature-settings => settings}/src/main/res/xml/settings_ui.xml (82%) rename feature/{feature-recordings-core => setup}/.gitignore (100%) rename feature/{feature-setup => setup}/build.gradle.kts (100%) rename feature/{feature-setup => setup}/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/SetupFragment.kt (100%) rename feature/{feature-setup => setup}/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenContent.kt (82%) rename feature/{feature-setup => setup}/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenState.kt (100%) rename feature/{feature-setup => setup}/src/main/kotlin/com/f0x1d/logfox/feature/setup/viewmodel/SetupViewModel.kt (94%) rename feature/{feature-setup => setup}/src/test/kotlin/com/f0x1d/logfox/setup/compose/SetupScreenContentTest.kt (100%) rename feature/{feature-setup => setup}/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png (100%) rename feature/{feature-setup => setup}/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png (100%) create mode 100644 feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png rename feature/{feature-setup => setup}/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png (68%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a24e863..6d01f6fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,34 +9,21 @@ android { defaultConfig { applicationId = logFoxPackageName - versionCode = 61 - versionName = "2.0.1" + versionCode = 62 + versionName = "2.0.2" } } dependencies { - implementation(project(":feature:feature-crashes")) - implementation(project(":feature:feature-crashes-core")) - implementation(project(":feature:feature-filters")) - implementation(project(":feature:feature-filters-core")) - implementation(project(":feature:feature-logging")) - implementation(project(":feature:feature-logging-core")) - implementation(project(":feature:feature-recordings")) - implementation(project(":feature:feature-recordings-core")) - implementation(project(":feature:feature-settings")) - implementation(project(":feature:feature-setup")) + implementation(projects.feature.crashes) + implementation(projects.feature.filters) + implementation(projects.feature.logging) + implementation(projects.feature.recordings) + implementation(projects.feature.settings) + implementation(projects.feature.setup) - implementation(libs.insetter) - implementation(libs.bundles.shizuku) implementation(libs.viewpump) - implementation(libs.gson) - - implementation(libs.glide) - ksp(libs.glide.compiler) - - implementation(libs.androidx.room) - implementation(libs.androidx.room.runtime) - ksp(libs.androidx.room.compiler) + implementation(libs.coil) implementation(libs.bundles.androidx) implementation(libs.material) diff --git a/app/src/main/kotlin/com/f0x1d/logfox/LogFoxApp.kt b/app/src/main/kotlin/com/f0x1d/logfox/LogFoxApp.kt index 1974d041..23d545b7 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/LogFoxApp.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/LogFoxApp.kt @@ -4,25 +4,36 @@ import android.app.Application import androidx.appcompat.app.AppCompatDelegate import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat -import com.f0x1d.logfox.context.LOGGING_STATUS_CHANNEL_ID -import com.f0x1d.logfox.context.RECORDING_STATUS_CHANNEL_ID -import com.f0x1d.logfox.context.notificationManagerCompat +import coil.ImageLoader +import coil.ImageLoaderFactory +import com.f0x1d.logfox.arch.LOGGING_STATUS_CHANNEL_ID +import com.f0x1d.logfox.arch.RECORDING_STATUS_CHANNEL_ID +import com.f0x1d.logfox.arch.notificationManagerCompat import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.google.android.material.color.DynamicColors +import com.google.android.material.color.DynamicColorsOptions import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp -class LogFoxApp: Application() { +class LogFoxApp: Application(), ImageLoaderFactory { @Inject lateinit var appPreferences: AppPreferences + @Inject + lateinit var imageLoader: ImageLoader + override fun onCreate() { super.onCreate() AppCompatDelegate.setDefaultNightMode(appPreferences.nightTheme) - DynamicColors.applyToActivitiesIfAvailable(this) + DynamicColors.applyToActivitiesIfAvailable( + this, + DynamicColorsOptions.Builder() + .setPrecondition { _, _ -> appPreferences.monetEnabled } + .build() + ) notificationManagerCompat.apply { val loggingStatusChannel = NotificationChannelCompat.Builder( @@ -51,4 +62,6 @@ class LogFoxApp: Application() { ) } } + + override fun newImageLoader(): ImageLoader = imageLoader } diff --git a/app/src/main/kotlin/com/f0x1d/logfox/coil/AppIconFetcher.kt b/app/src/main/kotlin/com/f0x1d/logfox/coil/AppIconFetcher.kt new file mode 100644 index 00000000..d48e3bc3 --- /dev/null +++ b/app/src/main/kotlin/com/f0x1d/logfox/coil/AppIconFetcher.kt @@ -0,0 +1,49 @@ +package com.f0x1d.logfox.coil + +import android.content.Context +import coil.ImageLoader +import coil.decode.DataSource +import coil.fetch.DrawableResult +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.request.Options +import com.f0x1d.logfox.arch.di.IODispatcher +import com.f0x1d.logfox.model.InstalledApp +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +class AppIconFetcher( + private val context: Context, + private val app: InstalledApp, + private val ioDispatcher: CoroutineDispatcher, +) : Fetcher { + + override suspend fun fetch(): FetchResult = withContext(ioDispatcher) { + val appDrawable = context.packageManager.getApplicationIcon(app.packageName) + + DrawableResult( + drawable = appDrawable, + isSampled = true, + dataSource = DataSource.DISK, + ) + } + + @Singleton + class Factory @Inject constructor( + @ApplicationContext private val context: Context, + @IODispatcher private val ioDispatcher: CoroutineDispatcher, + ) : Fetcher.Factory { + override fun create( + data: InstalledApp, + options: Options, + imageLoader: ImageLoader, + ): Fetcher = AppIconFetcher( + context = context, + app = data, + ioDispatcher = ioDispatcher, + ) + } +} diff --git a/app/src/main/kotlin/com/f0x1d/logfox/di/CoilModule.kt b/app/src/main/kotlin/com/f0x1d/logfox/di/CoilModule.kt new file mode 100644 index 00000000..4c15bc1f --- /dev/null +++ b/app/src/main/kotlin/com/f0x1d/logfox/di/CoilModule.kt @@ -0,0 +1,27 @@ +package com.f0x1d.logfox.di + +import android.content.Context +import coil.ImageLoader +import com.f0x1d.logfox.coil.AppIconFetcher +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CoilModule { + + @Provides + @Singleton + fun provideImageLoader( + @ApplicationContext context: Context, + appIconFetcherFactory: AppIconFetcher.Factory, + ): ImageLoader = ImageLoader.Builder(context) + .components { + add(appIconFetcherFactory) + } + .build() +} diff --git a/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt b/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt index 36814a00..2fcaa6ce 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt @@ -3,7 +3,7 @@ package com.f0x1d.logfox.di.logs import android.app.PendingIntent import android.content.Context import com.f0x1d.feature.logging.service.MainActivityPendingIntentProvider -import com.f0x1d.logfox.intents.makeActivityPendingIntent +import com.f0x1d.logfox.arch.makeActivityPendingIntent import com.f0x1d.logfox.ui.activity.MainActivity import dagger.Binds import dagger.Module diff --git a/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt b/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt index 49aacd8a..7269f5eb 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt @@ -2,7 +2,7 @@ package com.f0x1d.logfox.di.settings import android.content.Context import com.f0x1d.feature.logging.service.LoggingService -import com.f0x1d.logfox.context.sendService +import com.f0x1d.logfox.arch.sendService import com.f0x1d.logfox.feature.settings.LoggingServiceDelegate import dagger.Binds import dagger.Module diff --git a/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt b/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt index bc32455f..c26d6bf2 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt @@ -4,9 +4,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.f0x1d.feature.logging.service.LoggingService +import com.f0x1d.logfox.arch.hasPermissionToReadLogs import com.f0x1d.logfox.arch.startForegroundServiceAvailable -import com.f0x1d.logfox.context.hasPermissionToReadLogs -import com.f0x1d.logfox.context.toast +import com.f0x1d.logfox.arch.toast import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.terminals.ShizukuTerminal diff --git a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt b/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt index 35c8f993..95ee0055 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt @@ -21,15 +21,17 @@ import androidx.transition.TransitionManager import com.f0x1d.logfox.R import com.f0x1d.logfox.arch.contrastedNavBarAvailable import com.f0x1d.logfox.arch.gesturesAvailable +import com.f0x1d.logfox.arch.hasNotificationsPermission +import com.f0x1d.logfox.arch.isHorizontalOrientation import com.f0x1d.logfox.arch.ui.activity.BaseViewModelActivity -import com.f0x1d.logfox.context.hasNotificationsPermission -import com.f0x1d.logfox.context.isHorizontalOrientation +import com.f0x1d.logfox.arch.viewmodel.Event import com.f0x1d.logfox.databinding.ActivityMainBinding -import com.f0x1d.logfox.model.event.Event import com.f0x1d.logfox.navigation.Directions +import com.f0x1d.logfox.navigation.NavGraphs import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.Icons import com.f0x1d.logfox.viewmodel.MainViewModel +import com.f0x1d.logfox.viewmodel.OpenSetup import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -37,7 +39,14 @@ import dagger.hilt.android.AndroidEntryPoint class MainActivity: BaseViewModelActivity(), NavController.OnDestinationChangedListener { override val viewModel by viewModels() - private lateinit var navController: NavController + + private val navController by lazy { + val navHostFragment = supportFragmentManager.findFragmentById( + R.id.nav_host_fragment_content_main, + ) as NavHostFragment + + navHostFragment.navController + } private val requestNotificationPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -64,19 +73,13 @@ class MainActivity: BaseViewModelActivity(), @SuppressLint("InlinedApi") override fun ActivityMainBinding.onCreate(savedInstanceState: Bundle?) { - val navHostFragment = supportFragmentManager.findFragmentById( - R.id.nav_host_fragment_content_main, - ) as NavHostFragment - navController = navHostFragment.navController + setupNavigation() - barView?.setupWithNavController(navController) barView?.setOnItemReselectedListener { // Just do nothing } setupBarInsets() - navController.addOnDestinationChangedListener(this@MainActivity) - if (!hasNotificationsPermission() && !viewModel.askedNotificationsPermission) { MaterialAlertDialogBuilder(this@MainActivity) .setIcon(Icons.ic_dialog_notification_important) @@ -91,10 +94,22 @@ class MainActivity: BaseViewModelActivity(), viewModel.askedNotificationsPermission = true } + } - if (savedInstanceState == null && viewModel.openCrashesOnStartup) { - navController.navigate(Directions.crashes) + private fun ActivityMainBinding.setupNavigation() { + navController.graph = navController.navInflater.inflate(NavGraphs.nav_graph).apply { + setStartDestination( + startDestId = if (viewModel.openCrashesOnStartup) { + Directions.crashes + } else { + Directions.logs + }, + ) } + + barView?.setupWithNavController(navController) + + navController.addOnDestinationChangedListener(this@MainActivity) } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -125,8 +140,10 @@ class MainActivity: BaseViewModelActivity(), } override fun onEvent(event: Event) { - when (event.type) { - MainViewModel.EVENT_TYPE_SETUP -> navController.navigate(Directions.action_global_setupFragment) + super.onEvent(event) + + when (event) { + is OpenSetup -> navController.navigate(Directions.action_global_setupFragment) } } @@ -136,7 +153,7 @@ class MainActivity: BaseViewModelActivity(), Directions.logsExtendedCopyFragment -> false Directions.filtersFragment -> false Directions.editFilterFragment -> false - Directions.chooseAppFragment -> false + Directions.appsPickerFragment -> false Directions.appCrashesFragment -> false Directions.crashDetailsFragment -> false diff --git a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt b/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt index 8b82e6f4..3640e272 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt @@ -3,9 +3,10 @@ package com.f0x1d.logfox.viewmodel import android.app.Application import android.content.Intent import com.f0x1d.feature.logging.service.LoggingService +import com.f0x1d.logfox.arch.hasPermissionToReadLogs import com.f0x1d.logfox.arch.startForegroundServiceAvailable import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.context.hasPermissionToReadLogs +import com.f0x1d.logfox.arch.viewmodel.Event import com.f0x1d.logfox.preferences.shared.AppPreferences import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -16,10 +17,6 @@ class MainViewModel @Inject constructor( application: Application, ): BaseViewModel(application) { - companion object { - const val EVENT_TYPE_SETUP = "setup" - } - var askedNotificationsPermission get() = appPreferences.askedNotificationsPermission set(value) { appPreferences.askedNotificationsPermission = value } @@ -39,7 +36,9 @@ class MainViewModel @Inject constructor( ctx.startService(it) } } else { - sendEvent(EVENT_TYPE_SETUP) + sendEvent(OpenSetup) } } } + +data object OpenSetup : Event diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index a1337b65..89e5ad98 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -15,7 +15,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/navigation_rail" app:layout_constraintBottom_toBottomOf="parent" - app:navGraph="@navigation/nav_graph" tools:layout="@layout/fragment_logs" /> { } dependencies { + implementation(library("kotlinx-immutable-collections")) implementation(bundle("androidx-compose")) } } diff --git a/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt b/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt index 1837aef7..4543fbc5 100644 --- a/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt +++ b/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt @@ -8,20 +8,17 @@ internal fun DependencyHandlerScope.coreDependencies(withCompose: Boolean = true implementation(project(":data")) implementation(project(":strings")) - implementation(project(":core:core-arch")) - implementation(project(":core:core-context")) - implementation(project(":core:core-database")) - implementation(project(":core:core-datetime")) - implementation(project(":core:core-intents")) - implementation(project(":core:core-io")) - implementation(project(":core:core-navigation")) - implementation(project(":core:core-preferences")) - implementation(project(":core:core-terminals")) - implementation(project(":core:core-ui")) + implementation(project(":core:arch")) + implementation(project(":core:database")) + implementation(project(":core:datetime")) + implementation(project(":core:navigation")) + implementation(project(":core:preferences")) + implementation(project(":core:terminals")) + implementation(project(":core:ui")) if (withCompose) { - implementation(project(":core:core-ui-compose")) - testImplementation(project(":core:core-tests")) + implementation(project(":core:ui-compose")) + testImplementation(project(":core:tests")) } } diff --git a/core/core-arch/.gitignore b/core/arch/.gitignore similarity index 100% rename from core/core-arch/.gitignore rename to core/arch/.gitignore diff --git a/core/core-arch/build.gradle.kts b/core/arch/build.gradle.kts similarity index 100% rename from core/core-arch/build.gradle.kts rename to core/arch/build.gradle.kts diff --git a/core/core-context/src/main/AndroidManifest.xml b/core/arch/src/main/AndroidManifest.xml similarity index 66% rename from core/core-context/src/main/AndroidManifest.xml rename to core/arch/src/main/AndroidManifest.xml index acad04f9..f942e427 100644 --- a/core/core-context/src/main/AndroidManifest.xml +++ b/core/arch/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt similarity index 92% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt index 0f55769c..c7f192e0 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt @@ -20,3 +20,4 @@ import androidx.annotation.ChecksSdkIntAtLeast @get:ChecksSdkIntAtLeast(api = R) val canPickJSON = SDK_INT >= R @get:ChecksSdkIntAtLeast(api = S) val mutablePendingIntentAvailable = SDK_INT >= S +@get:ChecksSdkIntAtLeast(api = S) val monetAvailable = mutablePendingIntentAvailable diff --git a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/ContextExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ContextExt.kt similarity index 96% rename from core/core-context/src/main/kotlin/com/f0x1d/logfox/context/ContextExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ContextExt.kt index f43c9894..384a0ab9 100644 --- a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/ContextExt.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ContextExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.context +package com.f0x1d.logfox.arch import android.Manifest import android.annotation.SuppressLint @@ -15,7 +15,6 @@ import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import com.f0x1d.logfox.arch.shouldRequestNotificationsPermission import com.f0x1d.logfox.strings.Strings import java.io.File import kotlin.system.exitProcess diff --git a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/FileExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/FileExt.kt similarity index 88% rename from core/core-context/src/main/kotlin/com/f0x1d/logfox/context/FileExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/FileExt.kt index 8dea69bf..6e484ce1 100644 --- a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/FileExt.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/FileExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.context +package com.f0x1d.logfox.arch import android.content.Context import android.net.Uri diff --git a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/NotificationExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/NotificationExt.kt similarity index 81% rename from core/core-context/src/main/kotlin/com/f0x1d/logfox/context/NotificationExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/NotificationExt.kt index 5ad327b8..9a375494 100644 --- a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/NotificationExt.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/NotificationExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.context +package com.f0x1d.logfox.arch const val LOGGING_STATUS_CHANNEL_ID = "logging" const val RECORDING_STATUS_CHANNEL_ID = "recording" diff --git a/core/core-intents/src/main/kotlin/com/f0x1d/logfox/intents/PendingIntents.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/PendingIntents.kt similarity index 95% rename from core/core-intents/src/main/kotlin/com/f0x1d/logfox/intents/PendingIntents.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/PendingIntents.kt index 4fff33cc..a1acfad8 100644 --- a/core/core-intents/src/main/kotlin/com/f0x1d/logfox/intents/PendingIntents.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/PendingIntents.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.intents +package com.f0x1d.logfox.arch import android.app.Activity import android.app.PendingIntent @@ -7,7 +7,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Bundle -import com.f0x1d.logfox.arch.mutablePendingIntentAvailable const val CRASH_DETAILS_INTENT_ID = 0 const val COPY_CRASH_INTENT_ID = 1 diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/annotations/GsonSkip.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/annotations/GsonSkip.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/annotations/GsonSkip.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/annotations/GsonSkip.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/DispatchersModule.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/DispatchersModule.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/DispatchersModule.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/DispatchersModule.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/GsonModule.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/GsonModule.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/GsonModule.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/GsonModule.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/UtilsModule.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/UtilsModule.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/di/UtilsModule.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/di/UtilsModule.kt diff --git a/core/core-io/src/main/kotlin/com/f0x1d/logfox/io/OutputStreamExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/OutputStreamExt.kt similarity index 87% rename from core/core-io/src/main/kotlin/com/f0x1d/logfox/io/OutputStreamExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/OutputStreamExt.kt index bf213848..e5e9a72e 100644 --- a/core/core-io/src/main/kotlin/com/f0x1d/logfox/io/OutputStreamExt.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/OutputStreamExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.io +package com.f0x1d.logfox.arch.io import java.io.OutputStream import java.util.zip.ZipOutputStream diff --git a/core/core-io/src/main/kotlin/com/f0x1d/logfox/io/ZipOutputStreamExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/ZipOutputStreamExt.kt similarity index 93% rename from core/core-io/src/main/kotlin/com/f0x1d/logfox/io/ZipOutputStreamExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/ZipOutputStreamExt.kt index 6a3366a0..48086ea3 100644 --- a/core/core-io/src/main/kotlin/com/f0x1d/logfox/io/ZipOutputStreamExt.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/io/ZipOutputStreamExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.io +package com.f0x1d.logfox.arch.io import java.io.File import java.util.zip.ZipEntry diff --git a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/receiver/CopyReceiver.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/receiver/CopyReceiver.kt similarity index 79% rename from core/core-context/src/main/kotlin/com/f0x1d/logfox/context/receiver/CopyReceiver.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/receiver/CopyReceiver.kt index fd1c5b06..042876f3 100644 --- a/core/core-context/src/main/kotlin/com/f0x1d/logfox/context/receiver/CopyReceiver.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/receiver/CopyReceiver.kt @@ -1,11 +1,11 @@ -package com.f0x1d.logfox.context.receiver +package com.f0x1d.logfox.arch.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.f0x1d.logfox.context.copyText -import com.f0x1d.logfox.context.notificationManagerCompat -import com.f0x1d.logfox.context.toast +import com.f0x1d.logfox.arch.copyText +import com.f0x1d.logfox.arch.notificationManagerCompat +import com.f0x1d.logfox.arch.toast import com.f0x1d.logfox.strings.Strings class CopyReceiver: BroadcastReceiver() { diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/DatabaseProxyRepository.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/DatabaseProxyRepository.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/DatabaseProxyRepository.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/DatabaseProxyRepository.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt similarity index 96% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt index 24d057e4..79c1edb2 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding +import com.f0x1d.logfox.arch.ui.base.SimpleLifecycleOwner import com.f0x1d.logfox.arch.ui.enableEdgeToEdge import com.f0x1d.logfox.arch.ui.snackbar import dagger.hilt.EntryPoint @@ -14,7 +15,7 @@ import dev.chrisbanes.insetter.applyInsetter import io.github.inflationx.viewpump.ViewPump import io.github.inflationx.viewpump.ViewPumpContextWrapper -abstract class BaseActivity: AppCompatActivity() { +abstract class BaseActivity: AppCompatActivity(), SimpleLifecycleOwner { protected lateinit var binding: T diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt new file mode 100644 index 00000000..4fb01589 --- /dev/null +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt @@ -0,0 +1,24 @@ +package com.f0x1d.logfox.arch.ui.activity + +import android.os.Bundle +import androidx.viewbinding.ViewBinding +import com.f0x1d.logfox.arch.viewmodel.BaseViewModel +import com.f0x1d.logfox.arch.viewmodel.Event +import com.f0x1d.logfox.arch.viewmodel.ShowSnackbar + +abstract class BaseViewModelActivity: BaseActivity() { + + abstract val viewModel: T + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent) + } + + open fun onEvent(event: Event) { + when (event) { + is ShowSnackbar -> snackbar(event.text) + } + } +} diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt new file mode 100644 index 00000000..9259c03f --- /dev/null +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt @@ -0,0 +1,33 @@ +package com.f0x1d.logfox.arch.ui.base + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.launch + +interface SimpleLifecycleOwner { + + val lifecycle: Lifecycle + fun getViewLifecycleOwner(): LifecycleOwner? = null + + fun Flow.collectWithLifecycle( + state: Lifecycle.State = Lifecycle.State.STARTED, + collector: FlowCollector, + ) { + val viewLifecycleOwner = runCatching { getViewLifecycleOwner() }.getOrNull() + val lifecycle = viewLifecycleOwner?.lifecycle ?: lifecycle + + lifecycle.coroutineScope.launch { + lifecycle.repeatOnLifecycle(state) { + collect(collector) + } + } + } +} + +interface SimpleFragmentLifecycleOwner : SimpleLifecycleOwner { + override fun getViewLifecycleOwner(): LifecycleOwner +} diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt similarity index 78% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt index a7a61e0b..0cacad25 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt @@ -6,19 +6,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding +import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner import com.f0x1d.logfox.arch.ui.enableEdgeToEdge import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.launch -abstract class BaseBottomSheet: BottomSheetDialogFragment() { +abstract class BaseBottomSheet: BottomSheetDialogFragment(), SimpleFragmentLifecycleOwner { private var mutableBinding: T? = null protected val binding: T get() = mutableBinding!! @@ -49,15 +44,6 @@ abstract class BaseBottomSheet: BottomSheetDialogFragment() { return dialog } - protected fun Flow.collectWithLifecycle( - state: Lifecycle.State = Lifecycle.State.STARTED, - collector: FlowCollector, - ) = viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(state) { - collect(collector) - } - } - override fun onDestroyView() { super.onDestroyView() mutableBinding = null diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt similarity index 73% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt index 60f2a69c..f260ea79 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.view.View import androidx.viewbinding.ViewBinding import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.model.event.Event +import com.f0x1d.logfox.arch.viewmodel.Event abstract class BaseViewModelBottomSheet: BaseBottomSheet() { @@ -12,12 +12,7 @@ abstract class BaseViewModelBottomSheet: Bas override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - viewModel.eventsData.observe(viewLifecycleOwner) { - if (it.isConsumed) return@observe - - onEvent(it) - } + viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent) } open fun onEvent(event: Event) = Unit diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt similarity index 73% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt index 35aad80c..b5dd7197 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt @@ -6,17 +6,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding +import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner import com.f0x1d.logfox.arch.ui.snackbar import dev.chrisbanes.insetter.applyInsetter -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.launch -abstract class BaseFragment: Fragment() { +abstract class BaseFragment: Fragment(), SimpleFragmentLifecycleOwner { private var mutableBinding: T? = null protected val binding: T get() = mutableBinding!! @@ -37,15 +32,6 @@ abstract class BaseFragment: Fragment() { binding.onViewCreated(view, savedInstanceState) } - protected fun Flow.collectWithLifecycle( - state: Lifecycle.State = Lifecycle.State.STARTED, - collector: FlowCollector, - ) = viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(state) { - collect(collector) - } - } - override fun onDestroyView() { super.onDestroyView() mutableBinding = null diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt similarity index 51% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt index 305a8308..af4e719c 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt @@ -4,7 +4,8 @@ import android.os.Bundle import android.view.View import androidx.viewbinding.ViewBinding import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.model.event.Event +import com.f0x1d.logfox.arch.viewmodel.Event +import com.f0x1d.logfox.arch.viewmodel.ShowSnackbar abstract class BaseViewModelFragment: BaseFragment() { @@ -12,21 +13,12 @@ abstract class BaseViewModelFragment: BaseFr override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent) + } - viewModel.eventsData.observe(viewLifecycleOwner) { - if (it.isConsumed) return@observe - - onEvent(it) - } - - viewModel.snackbarEventsData.observe(viewLifecycleOwner) { - if (it.isConsumed) return@observe - - it.consume()?.also { message -> - snackbar(message) - } + open fun onEvent(event: Event) { + when (event) { + is ShowSnackbar -> snackbar(event.text) } } - - open fun onEvent(event: Event) = Unit } diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt similarity index 73% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt index dfd5e9d5..16301ef9 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding import com.f0x1d.logfox.arch.ui.fragment.BaseFragment @@ -19,17 +20,16 @@ abstract class BaseComposeFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.composeView.apply { - consumeWindowInsets = false - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - - setContent { - this@BaseComposeFragment.Content() - } - } + binding.composeView.setup { Content() } } @Composable abstract fun Content() } + +internal fun ComposeView.setup(content: @Composable () -> Unit) { + consumeWindowInsets = false + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent(content) +} diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt similarity index 74% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt index 28b38bee..c4b9e94a 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt @@ -4,9 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ViewCompositionStrategy import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment import com.f0x1d.logfox.arch.ui.snackbar @@ -23,15 +21,7 @@ abstract class BaseComposeViewModelFragment : BaseViewModelFr override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.composeView.apply { - consumeWindowInsets = false - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - - setContent { - this@BaseComposeViewModelFragment.Content() - } - } + binding.composeView.setup { Content() } } override fun snackbar(text: String): Snackbar = requireView().snackbar(text).apply { diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt similarity index 100% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt similarity index 93% rename from core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt index f5ed692d..da539adb 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt @@ -12,6 +12,7 @@ abstract class BaseStateViewModel( private val mutableUiState = MutableStateFlow(initialStateProvider()) val uiState = mutableUiState.asStateFlow() + val currentState: T = uiState.value protected fun state(block: T.() -> T) = mutableUiState.update(block) } diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt new file mode 100644 index 00000000..496497b2 --- /dev/null +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt @@ -0,0 +1,54 @@ +package com.f0x1d.logfox.arch.viewmodel + +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.f0x1d.logfox.strings.Strings +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +abstract class BaseViewModel( + application: Application, +) : AndroidViewModel(application) { + + private val eventsChannel = Channel(capacity = Channel.BUFFERED) + val eventsFlow = eventsChannel.receiveAsFlow() + + protected val ctx: Context get() = getApplication() + + protected fun launchCatching( + context: CoroutineContext = Dispatchers.Main, + errorBlock: suspend CoroutineScope.() -> Unit = {}, + block: suspend CoroutineScope.() -> Unit, + ) = viewModelScope.launch(context) { + try { + coroutineScope { + block(this) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + + errorBlock(this) + + e.printStackTrace() + + snackbar(ctx.getString(Strings.error, e.localizedMessage)) + } + } + + protected fun snackbar(id: Int) = snackbar(ctx.getString(id)) + protected fun snackbar(text: String) = sendEvent(ShowSnackbar(text)) + + protected fun sendEvent(event: Event) { + viewModelScope.launch { + eventsChannel.send(event) + } + } +} diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt new file mode 100644 index 00000000..43890554 --- /dev/null +++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt @@ -0,0 +1,7 @@ +package com.f0x1d.logfox.arch.viewmodel + +interface Event + +data class ShowSnackbar( + val text: String, +) : Event diff --git a/core/core-arch/src/main/res/color/navbar_transparent_background.xml b/core/arch/src/main/res/color/navbar_transparent_background.xml similarity index 100% rename from core/core-arch/src/main/res/color/navbar_transparent_background.xml rename to core/arch/src/main/res/color/navbar_transparent_background.xml diff --git a/core/core-arch/src/main/res/layout/fragment_compose.xml b/core/arch/src/main/res/layout/fragment_compose.xml similarity index 100% rename from core/core-arch/src/main/res/layout/fragment_compose.xml rename to core/arch/src/main/res/layout/fragment_compose.xml diff --git a/core/core-arch/src/main/res/values/colors.xml b/core/arch/src/main/res/values/colors.xml similarity index 100% rename from core/core-arch/src/main/res/values/colors.xml rename to core/arch/src/main/res/values/colors.xml diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/coroutines/flow/FlowExt.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/coroutines/flow/FlowExt.kt deleted file mode 100644 index b0d9211a..00000000 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/coroutines/flow/FlowExt.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.f0x1d.logfox.arch.coroutines.flow - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -inline fun MutableStateFlow>.updateList(block: MutableList.() -> Unit) = update { - it.toMutableList().apply(block) -} diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt deleted file mode 100644 index 3030473d..00000000 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.f0x1d.logfox.arch.ui.activity - -import android.os.Bundle -import androidx.viewbinding.ViewBinding -import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.model.event.Event - -abstract class BaseViewModelActivity: BaseActivity() { - - abstract val viewModel: T - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - viewModel.eventsData.observe(this) { - if (it.isConsumed) return@observe - - onEvent(it) - } - - viewModel.snackbarEventsData.observe(this) { - if (it.isConsumed) return@observe - - it.consume()?.also { message -> - snackbar(message) - } - } - } - - open fun onEvent(event: Event) = Unit -} diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt deleted file mode 100644 index 7a737a8c..00000000 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.f0x1d.logfox.arch.viewmodel - -import android.app.Application -import android.content.Context -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.model.event.Event -import com.f0x1d.logfox.model.event.NoDataEvent -import com.f0x1d.logfox.model.event.SnackbarEvent -import com.f0x1d.logfox.strings.Strings -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlin.coroutines.CoroutineContext - -abstract class BaseViewModel(application: Application): AndroidViewModel(application) { - - val ctx: Context get() = getApplication() - - val eventsData = MutableLiveData() - val snackbarEventsData = MutableLiveData() - - fun launchCatching( - context: CoroutineContext = Dispatchers.Main, - errorBlock: suspend CoroutineScope.() -> Unit = {}, - block: suspend CoroutineScope.() -> Unit - ) = viewModelScope.launch(context) { - try { - coroutineScope { - block(this) - } - } catch (e: Exception) { - if (e is CancellationException) return@launch - - errorBlock(this) - - e.printStackTrace() - - snackbar(ctx.getString(Strings.error, e.localizedMessage)) - } - } - - protected fun snackbar(id: Int) = snackbar(ctx.getString(id)) - - protected fun snackbar(text: String) { - viewModelScope.launch(Dispatchers.Main.immediate) { - snackbarEventsData.value = SnackbarEvent(text) - } - } - - protected fun sendEvent(type: String, data: Any) = eventsData.sendEvent(type, data) - protected fun sendEvent(type: String) = eventsData.sendEvent(type) - - private fun MutableLiveData.sendEvent(type: String, data: Any) = postValue(Event(type, data)) - private fun MutableLiveData.sendEvent(type: String) = postValue(NoDataEvent(type)) -} diff --git a/core/core-context/build.gradle.kts b/core/core-context/build.gradle.kts deleted file mode 100644 index 3f1d6817..00000000 --- a/core/core-context/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id("logfox.android.core") - id("logfox.android.hilt") -} - -android.namespace = "com.f0x1d.logfox.context" - -dependencies { - implementation(project(":core:core-arch")) -} diff --git a/core/core-intents/build.gradle.kts b/core/core-intents/build.gradle.kts deleted file mode 100644 index ccb4026a..00000000 --- a/core/core-intents/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("logfox.android.core") -} - -android.namespace = "com.f0x1d.logfox.intents" - -dependencies { - implementation(project(":core:core-arch")) -} diff --git a/core/core-io/build.gradle.kts b/core/core-io/build.gradle.kts deleted file mode 100644 index 2ea04906..00000000 --- a/core/core-io/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("logfox.android.core") -} - -android.namespace = "com.f0x1d.logfox.io" - -dependencies { - -} diff --git a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt b/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt deleted file mode 100644 index 286f1ba1..00000000 --- a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.f0x1d.logfox.ui.compose.component.button - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.f0x1d.logfox.strings.Strings -import com.f0x1d.logfox.ui.Icons -import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme - -@Composable -fun RichButton( - @StringRes text: Int, - @DrawableRes icon: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - RichButton( - modifier = modifier, - text = stringResource(id = text), - painter = painterResource(id = icon), - onClick = onClick, - ) -} - -@Composable -fun RichButton( - text: String, - painter: Painter, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Button( - modifier = modifier, - onClick = onClick, - ) { - Icon( - painter = painter, - contentDescription = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = text, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - ) - } -} - -@Preview -@Composable -private fun RichButtonAdbPreview() { - LogFoxTheme { - RichButton( - text = Strings.adb, - icon = Icons.ic_adb, - onClick = { }, - ) - } -} - -@Preview -@Composable -private fun RichButtonRootPreview() { - LogFoxTheme { - RichButton( - text = "Root", - painter = painterResource(id = Icons.ic_square_root), - onClick = { }, - ) - } -} diff --git a/core/core-context/.gitignore b/core/database/.gitignore similarity index 100% rename from core/core-context/.gitignore rename to core/database/.gitignore diff --git a/core/core-database/build.gradle.kts b/core/database/build.gradle.kts similarity index 89% rename from core/core-database/build.gradle.kts rename to core/database/build.gradle.kts index 7b0bd0f3..83c12ac8 100644 --- a/core/core-database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -14,7 +14,7 @@ android { } dependencies { - implementation(project(":core:core-arch")) + implementation(projects.core.arch) implementation(libs.androidx.room) implementation(libs.androidx.room.runtime) diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/10.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/10.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/10.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/10.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/11.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/11.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/11.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/11.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/12.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/12.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/12.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/12.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/13.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/13.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/13.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/13.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/14.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/14.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/14.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/14.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/15.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/15.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/15.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/15.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/16.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/16.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/16.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/16.json diff --git a/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/17.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/17.json new file mode 100644 index 00000000..a510d3e5 --- /dev/null +++ b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/17.json @@ -0,0 +1,254 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "a2c925fa72fa2086ac30fe2fde7ce585", + "entities": [ + { + "tableName": "AppCrash", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_name` TEXT, `package_name` TEXT NOT NULL, `crash_type` INTEGER NOT NULL, `date_and_time` INTEGER NOT NULL, `log` TEXT NOT NULL, `log_file` TEXT, `log_dump_file` TEXT, `is_deleted` INTEGER NOT NULL DEFAULT 0, `deleted_time` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "appName", + "columnName": "app_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "crashType", + "columnName": "crash_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateAndTime", + "columnName": "date_and_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "log", + "columnName": "log", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logFile", + "columnName": "log_file", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logDumpFile", + "columnName": "log_dump_file", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDeleted", + "columnName": "is_deleted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deletedTime", + "columnName": "deleted_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AppCrash_date_and_time", + "unique": false, + "columnNames": [ + "date_and_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AppCrash_date_and_time` ON `${TABLE_NAME}` (`date_and_time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "LogRecording", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `date_and_time` INTEGER NOT NULL, `file` TEXT NOT NULL, `is_cache_recording` INTEGER NOT NULL DEFAULT 0, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateAndTime", + "columnName": "date_and_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "file", + "columnName": "file", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isCacheRecording", + "columnName": "is_cache_recording", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserFilter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`including` INTEGER NOT NULL, `allowed_levels` TEXT NOT NULL, `uid` TEXT, `pid` TEXT, `tid` TEXT, `package_name` TEXT, `tag` TEXT, `content` TEXT, `enabled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "including", + "columnName": "including", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedLevels", + "columnName": "allowed_levels", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pid", + "columnName": "pid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tid", + "columnName": "tid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DisabledApp", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DisabledApp_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DisabledApp_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a2c925fa72fa2086ac30fe2fde7ce585')" + ] + } +} \ No newline at end of file diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/7.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/7.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/7.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/7.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/8.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/8.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/8.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/8.json diff --git a/core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/9.json b/core/database/schemas/com.f0x1d.logfox.database.AppDatabase/9.json similarity index 100% rename from core/core-database/schemas/com.f0x1d.logfox.database.AppDatabase/9.json rename to core/database/schemas/com.f0x1d.logfox.database.AppDatabase/9.json diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt similarity index 92% rename from core/core-database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt rename to core/database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt index d6e0db5d..7c3aac13 100644 --- a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt +++ b/core/database/src/main/kotlin/com/f0x1d/logfox/database/AppDatabase.kt @@ -11,6 +11,8 @@ import com.f0x1d.logfox.database.entity.AllowedLevelsConverter import com.f0x1d.logfox.database.entity.AppCrash import com.f0x1d.logfox.database.entity.AppCrashDao import com.f0x1d.logfox.database.entity.CrashTypeConverter +import com.f0x1d.logfox.database.entity.DisabledApp +import com.f0x1d.logfox.database.entity.DisabledAppDao import com.f0x1d.logfox.database.entity.FileConverter import com.f0x1d.logfox.database.entity.LogRecording import com.f0x1d.logfox.database.entity.LogRecordingDao @@ -22,8 +24,9 @@ import com.f0x1d.logfox.database.entity.UserFilterDao AppCrash::class, LogRecording::class, UserFilter::class, + DisabledApp::class, ], - version = 16, + version = 17, autoMigrations = [ AutoMigration( from = 12, @@ -42,6 +45,10 @@ import com.f0x1d.logfox.database.entity.UserFilterDao from = 15, to = 16, ), + AutoMigration( + from = 16, + to = 17, + ), ] ) @TypeConverters( @@ -92,4 +99,5 @@ abstract class AppDatabase: RoomDatabase() { abstract fun appCrashes(): AppCrashDao abstract fun logRecordings(): LogRecordingDao abstract fun userFilters(): UserFilterDao + abstract fun disabledApps(): DisabledAppDao } diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/di/RoomModule.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/di/RoomModule.kt similarity index 100% rename from core/core-database/src/main/kotlin/com/f0x1d/logfox/database/di/RoomModule.kt rename to core/database/src/main/kotlin/com/f0x1d/logfox/database/di/RoomModule.kt diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt similarity index 100% rename from core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt rename to core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt diff --git a/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/DisabledApp.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/DisabledApp.kt new file mode 100644 index 00000000..994c0a11 --- /dev/null +++ b/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/DisabledApp.kt @@ -0,0 +1,55 @@ +package com.f0x1d.logfox.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Entity +data class DisabledApp( + @ColumnInfo(name = "package_name", index = true) val packageName: String, + @PrimaryKey(autoGenerate = true) val id: Long = 0, +) + +@Dao +interface DisabledAppDao { + + @Query("SELECT * FROM DisabledApp") + suspend fun getAll(): List + + @Query("SELECT * FROM DisabledApp") + fun getAllAsFlow(): Flow> + + @Query("SELECT * FROM DisabledApp WHERE id = :id") + suspend fun getById(id: Long): DisabledApp? + + @Query("SELECT * FROM DisabledApp WHERE id = :id") + fun getByIdAsFlow(id: Long): Flow + + @Query("SELECT * FROM DisabledApp WHERE package_name = :packageName") + suspend fun getByPackageName(packageName: String): DisabledApp? + + @Query("SELECT * FROM DisabledApp WHERE package_name = :packageName") + fun getByPackageNameAsFlow(packageName: String): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(item: DisabledApp) + + @Update + suspend fun update(item: DisabledApp) + + @Delete + suspend fun delete(item: DisabledApp) + + @Query("DELETE FROM DisabledApp WHERE package_name = :packageName") + suspend fun deleteByPackageName(packageName: String) + + @Query("DELETE FROM DisabledApp") + suspend fun deleteAll() +} diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/LogRecording.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/LogRecording.kt similarity index 100% rename from core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/LogRecording.kt rename to core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/LogRecording.kt diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/UserFilter.kt b/core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/UserFilter.kt similarity index 100% rename from core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/UserFilter.kt rename to core/database/src/main/kotlin/com/f0x1d/logfox/database/entity/UserFilter.kt diff --git a/core/core-database/.gitignore b/core/datetime/.gitignore similarity index 100% rename from core/core-database/.gitignore rename to core/datetime/.gitignore diff --git a/core/core-datetime/build.gradle.kts b/core/datetime/build.gradle.kts similarity index 72% rename from core/core-datetime/build.gradle.kts rename to core/datetime/build.gradle.kts index f36d9903..6b984442 100644 --- a/core/core-datetime/build.gradle.kts +++ b/core/datetime/build.gradle.kts @@ -6,5 +6,5 @@ plugins { android.namespace = "com.f0x1d.logfox.datetime" dependencies { - implementation(project(":core:core-preferences")) + implementation(projects.core.preferences) } diff --git a/core/core-datetime/src/main/kotlin/com/f0x1d/logfox/datetime/ContextExt.kt b/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/ContextExt.kt similarity index 100% rename from core/core-datetime/src/main/kotlin/com/f0x1d/logfox/datetime/ContextExt.kt rename to core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/ContextExt.kt diff --git a/core/core-datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt b/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt similarity index 71% rename from core/core-datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt rename to core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt index e94802dc..9267e1af 100644 --- a/core/core-datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt +++ b/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatter.kt @@ -8,18 +8,25 @@ import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale import javax.inject.Inject -class DateTimeFormatter @Inject constructor( +interface DateTimeFormatter { + fun formatDate(time: Long): String + fun formatTime(time: Long): String + + fun formatForExport(time: Long): String +} + +internal class DateTimeFormatterImpl @Inject constructor( @ApplicationContext private val context: Context, private val appPreferences: AppPreferences, -) { +) : DateTimeFormatter { private val dateFormatter by lazy { createFormatter(appPreferences.dateFormat) } private val timeFormatter by lazy { createFormatter(appPreferences.timeFormat) } - fun formatDate(time: Long): String = tryFormatBy(dateFormatter, time) - fun formatTime(time: Long): String = tryFormatBy(timeFormatter, time) + override fun formatDate(time: Long): String = tryFormatBy(dateFormatter, time) + override fun formatTime(time: Long): String = tryFormatBy(timeFormatter, time) - fun formatForExport(time: Long) = formatDate(time) + override fun formatForExport(time: Long) = formatDate(time) .withReplacedBadSymbolsForFileName + "-" + formatTime(time) .withReplacedBadSymbolsForFileName diff --git a/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatterModule.kt b/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatterModule.kt new file mode 100644 index 00000000..0881f586 --- /dev/null +++ b/core/datetime/src/main/kotlin/com/f0x1d/logfox/datetime/DateTimeFormatterModule.kt @@ -0,0 +1,16 @@ +package com.f0x1d.logfox.datetime + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface DateTimeFormatterModule { + + @Binds + fun bindDateTimeFormatter( + dateTimeFormatterImpl: DateTimeFormatterImpl, + ): DateTimeFormatter +} diff --git a/core/core-datetime/.gitignore b/core/navigation/.gitignore similarity index 100% rename from core/core-datetime/.gitignore rename to core/navigation/.gitignore diff --git a/core/core-navigation/build.gradle.kts b/core/navigation/build.gradle.kts similarity index 100% rename from core/core-navigation/build.gradle.kts rename to core/navigation/build.gradle.kts diff --git a/core/core-navigation/src/main/kotlin/com/f0x1d/logfox/navigation/Navigation.kt b/core/navigation/src/main/kotlin/com/f0x1d/logfox/navigation/Navigation.kt similarity index 100% rename from core/core-navigation/src/main/kotlin/com/f0x1d/logfox/navigation/Navigation.kt rename to core/navigation/src/main/kotlin/com/f0x1d/logfox/navigation/Navigation.kt diff --git a/core/core-navigation/src/main/res/navigation/crashes.xml b/core/navigation/src/main/res/navigation/crashes.xml similarity index 80% rename from core/core-navigation/src/main/res/navigation/crashes.xml rename to core/navigation/src/main/res/navigation/crashes.xml index 01487aae..0e44bf32 100644 --- a/core/core-navigation/src/main/res/navigation/crashes.xml +++ b/core/navigation/src/main/res/navigation/crashes.xml @@ -22,6 +22,13 @@ app:exitAnim="@anim/nav_default_exit_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popExitAnim="@anim/nav_default_pop_exit_anim"/> + + + diff --git a/core/core-navigation/src/main/res/navigation/logs.xml b/core/navigation/src/main/res/navigation/logs.xml similarity index 90% rename from core/core-navigation/src/main/res/navigation/logs.xml rename to core/navigation/src/main/res/navigation/logs.xml index a88ee4be..464a692c 100644 --- a/core/core-navigation/src/main/res/navigation/logs.xml +++ b/core/navigation/src/main/res/navigation/logs.xml @@ -68,15 +68,15 @@ android:defaultValue="-1L" /> + android:id="@+id/appsPickerFragment" + android:name="com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.AppsPickerFragment" + android:label="AppsPickerFragment" /> diff --git a/core/core-navigation/src/main/res/navigation/nav_graph.xml b/core/navigation/src/main/res/navigation/nav_graph.xml similarity index 100% rename from core/core-navigation/src/main/res/navigation/nav_graph.xml rename to core/navigation/src/main/res/navigation/nav_graph.xml diff --git a/core/core-navigation/src/main/res/navigation/recordings.xml b/core/navigation/src/main/res/navigation/recordings.xml similarity index 100% rename from core/core-navigation/src/main/res/navigation/recordings.xml rename to core/navigation/src/main/res/navigation/recordings.xml diff --git a/core/core-navigation/src/main/res/navigation/settings.xml b/core/navigation/src/main/res/navigation/settings.xml similarity index 100% rename from core/core-navigation/src/main/res/navigation/settings.xml rename to core/navigation/src/main/res/navigation/settings.xml diff --git a/core/core-intents/.gitignore b/core/preferences/.gitignore similarity index 100% rename from core/core-intents/.gitignore rename to core/preferences/.gitignore diff --git a/core/core-preferences/build.gradle.kts b/core/preferences/build.gradle.kts similarity index 78% rename from core/core-preferences/build.gradle.kts rename to core/preferences/build.gradle.kts index 4aa13ca2..02384c1a 100644 --- a/core/core-preferences/build.gradle.kts +++ b/core/preferences/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android.namespace = "com.f0x1d.logfox.preferences" dependencies { - implementation(project(":core:core-database")) + implementation(projects.core.database) implementation(libs.flow.preferences) } diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt b/core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt similarity index 96% rename from core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt rename to core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt index 23fef81e..b0ffe54c 100644 --- a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt +++ b/core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt @@ -41,6 +41,10 @@ class AppPreferences @Inject constructor( defaultValue = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, ).asFlow() + var monetEnabled + get() = get("pref_monet_enabled", true) + set(value) { put("pref_monet_enabled", value) } + var dateFormat get() = getNullable("pref_date_format", DATE_FORMAT_DEFAULT) set(value) { put("pref_date_format", value) } @@ -85,6 +89,9 @@ class AppPreferences @Inject constructor( var exportLogsInOriginalFormat get() = get("pref_export_logs_in_original_format", true) set(value) { put("pref_export_logs_in_original_format", value) } + var wrapCrashLogLines + get() = get("pref_wrap_crash_log_lines", true) + set(value) { put("pref_wrap_crash_log_lines", value) } var showLogDate get() = get("pref_show_log_date", false) diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/ContextExt.kt b/core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/ContextExt.kt similarity index 100% rename from core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/ContextExt.kt rename to core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/ContextExt.kt diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/base/BasePreferences.kt b/core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/base/BasePreferences.kt similarity index 100% rename from core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/base/BasePreferences.kt rename to core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/base/BasePreferences.kt diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt b/core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt similarity index 100% rename from core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt rename to core/preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt diff --git a/core/core-io/.gitignore b/core/terminals/.gitignore similarity index 100% rename from core/core-io/.gitignore rename to core/terminals/.gitignore diff --git a/core/core-terminals/build.gradle.kts b/core/terminals/build.gradle.kts similarity index 84% rename from core/core-terminals/build.gradle.kts rename to core/terminals/build.gradle.kts index 27f10874..1b4c2aae 100644 --- a/core/core-terminals/build.gradle.kts +++ b/core/terminals/build.gradle.kts @@ -10,7 +10,7 @@ android { } dependencies { - implementation(project(":core:core-arch")) + implementation(projects.core.arch) implementation(libs.libsu) implementation(libs.bundles.shizuku) diff --git a/core/core-terminals/src/main/AndroidManifest.xml b/core/terminals/src/main/AndroidManifest.xml similarity index 100% rename from core/core-terminals/src/main/AndroidManifest.xml rename to core/terminals/src/main/AndroidManifest.xml diff --git a/core/core-terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl b/core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl similarity index 100% rename from core/core-terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl rename to core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl diff --git a/core/core-terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl b/core/terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl similarity index 100% rename from core/core-terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl rename to core/terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/di/TerminalsModule.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/di/TerminalsModule.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/di/TerminalsModule.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/di/TerminalsModule.kt diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt diff --git a/core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt similarity index 100% rename from core/core-terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt diff --git a/core/core-terminals/src/main/res/values-it/strings.xml b/core/terminals/src/main/res/values-it/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values-it/strings.xml rename to core/terminals/src/main/res/values-it/strings.xml diff --git a/core/core-terminals/src/main/res/values-pt-rBR/strings.xml b/core/terminals/src/main/res/values-pt-rBR/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values-pt-rBR/strings.xml rename to core/terminals/src/main/res/values-pt-rBR/strings.xml diff --git a/core/core-terminals/src/main/res/values-ru/strings.xml b/core/terminals/src/main/res/values-ru/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values-ru/strings.xml rename to core/terminals/src/main/res/values-ru/strings.xml diff --git a/core/core-terminals/src/main/res/values-tr/strings.xml b/core/terminals/src/main/res/values-tr/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values-tr/strings.xml rename to core/terminals/src/main/res/values-tr/strings.xml diff --git a/core/core-terminals/src/main/res/values-zh-rCN/strings.xml b/core/terminals/src/main/res/values-zh-rCN/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values-zh-rCN/strings.xml rename to core/terminals/src/main/res/values-zh-rCN/strings.xml diff --git a/core/core-terminals/src/main/res/values/strings.xml b/core/terminals/src/main/res/values/strings.xml similarity index 100% rename from core/core-terminals/src/main/res/values/strings.xml rename to core/terminals/src/main/res/values/strings.xml diff --git a/core/core-navigation/.gitignore b/core/tests/.gitignore similarity index 100% rename from core/core-navigation/.gitignore rename to core/tests/.gitignore diff --git a/core/core-tests/build.gradle.kts b/core/tests/build.gradle.kts similarity index 100% rename from core/core-tests/build.gradle.kts rename to core/tests/build.gradle.kts diff --git a/core/core-tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt b/core/tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt similarity index 97% rename from core/core-tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt rename to core/tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt index c2438b5a..67e1ecc1 100644 --- a/core/core-tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt +++ b/core/tests/src/main/kotlin/com/f0x1d/logfox/core/tests/ScreenshotTest.kt @@ -42,7 +42,7 @@ abstract class ScreenshotTest { whatToCapture: SemanticsNodeInteractionsProvider.() -> SemanticsNodeInteraction = { onRoot() }, content: @Composable () -> Unit, ) { - composeRule.setContent { content() } + composeRule.setContent(content) composeRule.actions() waitForIdle() diff --git a/core/core-tests/src/main/kotlin/com/f0x1d/logfox/core/tests/compose/SemanticsNodeInteractionsProviderExt.kt b/core/tests/src/main/kotlin/com/f0x1d/logfox/core/tests/compose/SemanticsNodeInteractionsProviderExt.kt similarity index 100% rename from core/core-tests/src/main/kotlin/com/f0x1d/logfox/core/tests/compose/SemanticsNodeInteractionsProviderExt.kt rename to core/tests/src/main/kotlin/com/f0x1d/logfox/core/tests/compose/SemanticsNodeInteractionsProviderExt.kt diff --git a/core/core-preferences/.gitignore b/core/ui-compose/.gitignore similarity index 100% rename from core/core-preferences/.gitignore rename to core/ui-compose/.gitignore diff --git a/core/core-ui-compose/build.gradle.kts b/core/ui-compose/build.gradle.kts similarity index 76% rename from core/core-ui-compose/build.gradle.kts rename to core/ui-compose/build.gradle.kts index 7d484bd7..1a171d32 100644 --- a/core/core-ui-compose/build.gradle.kts +++ b/core/ui-compose/build.gradle.kts @@ -6,5 +6,5 @@ plugins { android.namespace = "com.f0x1d.logfox.ui.compose" dependencies { - implementation(project(":core:core-ui")) + implementation(projects.core.ui) } diff --git a/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/NavigationBackButton.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/NavigationBackButton.kt new file mode 100644 index 00000000..881d0ccd --- /dev/null +++ b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/NavigationBackButton.kt @@ -0,0 +1,32 @@ +package com.f0x1d.logfox.ui.compose.component.button + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.f0x1d.logfox.ui.Icons +import com.f0x1d.logfox.ui.compose.preview.DayNightPreview +import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme + +@Composable +fun NavigationBackButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier, + onClick = onClick, + ) { + Icon( + painter = painterResource(id = Icons.ic_arrow_back), + contentDescription = null, + ) + } +} + +@DayNightPreview +@Composable +private fun Preview() = LogFoxTheme { + NavigationBackButton(onClick = { }) +} diff --git a/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt new file mode 100644 index 00000000..978aa7f3 --- /dev/null +++ b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/button/RichButton.kt @@ -0,0 +1,54 @@ +package com.f0x1d.logfox.ui.compose.component.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.f0x1d.logfox.ui.Icons +import com.f0x1d.logfox.ui.compose.preview.DayNightPreview +import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme + +@Composable +fun RichButton( + text: @Composable () -> Unit, + icon: @Composable () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + onClick = onClick, + ) { + Box(modifier = Modifier.size(24.dp)) { + icon() + } + Spacer(modifier = Modifier.width(8.dp)) + ProvideTextStyle(value = MaterialTheme.typography.bodyLarge) { + text() + } + } +} + +@DayNightPreview +@Composable +private fun RichButtonAdbPreview() = LogFoxTheme { + RichButton( + text = { Text(text = "ADB") }, + icon = { + Icon( + painter = painterResource(id = Icons.ic_adb), + contentDescription = null, + ) + }, + onClick = { }, + ) +} diff --git a/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/search/TopSearchBar.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/search/TopSearchBar.kt new file mode 100644 index 00000000..0b8221b4 --- /dev/null +++ b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/component/search/TopSearchBar.kt @@ -0,0 +1,65 @@ +package com.f0x1d.logfox.ui.compose.component.search + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopSearchBar( + query: String, + onQueryChange: (String) -> Unit, + onSearch: (String) -> Unit, + active: Boolean, + onActiveChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + verticalPadding: Dp = DefaultVerticalPadding, + horizontalPadding: Dp = DefaultHorizontalPadding, + enabled: Boolean = true, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + Surface(modifier = modifier) { + val searchBarVerticalPadding by animateDpAsState( + targetValue = if (active) 0.dp else verticalPadding, + label = "Search bar vertical padding", + ) + val searchBarHorizontalPadding by animateDpAsState( + targetValue = if (active) 0.dp else horizontalPadding, + label = "Search bar horizontal padding", + ) + + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = searchBarVerticalPadding, + horizontal = searchBarHorizontalPadding, + ), + query = query, + onQueryChange = onQueryChange, + onSearch = onSearch, + active = active, + onActiveChange = onActiveChange, + enabled = enabled, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + content = content, + ) + } +} + +private val DefaultVerticalPadding = 10.dp +private val DefaultHorizontalPadding = 15.dp diff --git a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/preview/DayNightPreview.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/preview/DayNightPreview.kt similarity index 100% rename from core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/preview/DayNightPreview.kt rename to core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/preview/DayNightPreview.kt diff --git a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Color.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Color.kt similarity index 100% rename from core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Color.kt rename to core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Color.kt diff --git a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Theme.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Theme.kt similarity index 100% rename from core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Theme.kt rename to core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Theme.kt diff --git a/core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Type.kt b/core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Type.kt similarity index 100% rename from core/core-ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Type.kt rename to core/ui-compose/src/main/kotlin/com/f0x1d/logfox/ui/compose/theme/Type.kt diff --git a/core/core-ui-compose/src/main/res/values/font_certs.xml b/core/ui-compose/src/main/res/values/font_certs.xml similarity index 100% rename from core/core-ui-compose/src/main/res/values/font_certs.xml rename to core/ui-compose/src/main/res/values/font_certs.xml diff --git a/core/core-ui/.gitignore b/core/ui/.gitignore similarity index 100% rename from core/core-ui/.gitignore rename to core/ui/.gitignore diff --git a/core/core-ui/build.gradle.kts b/core/ui/build.gradle.kts similarity index 71% rename from core/core-ui/build.gradle.kts rename to core/ui/build.gradle.kts index bf1969d3..8a5682c3 100644 --- a/core/core-ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -6,8 +6,8 @@ plugins { android.namespace = "com.f0x1d.logfox.ui" dependencies { - implementation(project(":core:core-context")) - implementation(project(":core:core-preferences")) + implementation(projects.core.arch) + implementation(projects.core.preferences) implementation(libs.insetter) implementation(libs.viewpump) diff --git a/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Colors.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Colors.kt new file mode 100644 index 00000000..17b78dc7 --- /dev/null +++ b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Colors.kt @@ -0,0 +1,3 @@ +package com.f0x1d.logfox.ui + +typealias Colors = R.color diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/Icons.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Icons.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/Icons.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Icons.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/density/PxExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/density/PxExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/density/PxExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/density/PxExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/di/ViewPumpModule.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/di/ViewPumpModule.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/di/ViewPumpModule.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/di/ViewPumpModule.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/dialog/AreYouSureDialogExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/dialog/AreYouSureDialogExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/dialog/AreYouSureDialogExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/dialog/AreYouSureDialogExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconDataFetcher.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconDataFetcher.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconDataFetcher.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconDataFetcher.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconGlideModule.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconGlideModule.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconGlideModule.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconGlideModule.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoader.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoader.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoader.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoader.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoaderFactory.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoaderFactory.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoaderFactory.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/glide/icon/IconModelLoaderFactory.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/interceptor/FontsInterceptor.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/interceptor/FontsInterceptor.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/interceptor/FontsInterceptor.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/interceptor/FontsInterceptor.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomApplyInsetsNavigationRailView.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomApplyInsetsNavigationRailView.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomApplyInsetsNavigationRailView.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomApplyInsetsNavigationRailView.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomNestedScrollView.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomNestedScrollView.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomNestedScrollView.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/CustomNestedScrollView.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/FABExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/FABExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/FABExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/FABExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ImageViewExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ImageViewExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ImageViewExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ImageViewExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/MenuExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/MenuExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/MenuExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/MenuExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/OnlyUserCheckedChangeListener.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/OnlyUserCheckedChangeListener.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/OnlyUserCheckedChangeListener.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/OnlyUserCheckedChangeListener.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt similarity index 97% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt index 0ec077d9..f69cea29 100644 --- a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt +++ b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/PreferenceExt.kt @@ -3,7 +3,7 @@ package com.f0x1d.logfox.ui.view import android.view.LayoutInflater import android.view.inputmethod.InputMethodManager import androidx.preference.Preference -import com.f0x1d.logfox.context.inputMethodManager +import com.f0x1d.logfox.arch.inputMethodManager import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.databinding.DialogTextBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ToolbarExt.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ToolbarExt.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ToolbarExt.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/ToolbarExt.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelExtensions.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelExtensions.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelExtensions.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelExtensions.kt diff --git a/core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelView.kt b/core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelView.kt similarity index 100% rename from core/core-ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelView.kt rename to core/ui/src/main/kotlin/com/f0x1d/logfox/ui/view/loglevel/LogLevelView.kt diff --git a/core/core-ui/src/main/res/color/item_log_background_ripple.xml b/core/ui/src/main/res/color/item_log_background_ripple.xml similarity index 100% rename from core/core-ui/src/main/res/color/item_log_background_ripple.xml rename to core/ui/src/main/res/color/item_log_background_ripple.xml diff --git a/core/core-ui/src/main/res/drawable-ldrtl/item_log_level_background.xml b/core/ui/src/main/res/drawable-ldrtl/item_log_level_background.xml similarity index 100% rename from core/core-ui/src/main/res/drawable-ldrtl/item_log_level_background.xml rename to core/ui/src/main/res/drawable-ldrtl/item_log_level_background.xml diff --git a/core/core-ui/src/main/res/drawable/ic_adb.xml b/core/ui/src/main/res/drawable/ic_adb.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_adb.xml rename to core/ui/src/main/res/drawable/ic_adb.xml diff --git a/core/core-ui/src/main/res/drawable/ic_add.xml b/core/ui/src/main/res/drawable/ic_add.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_add.xml rename to core/ui/src/main/res/drawable/ic_add.xml diff --git a/core/core-ui/src/main/res/drawable/ic_add_link.xml b/core/ui/src/main/res/drawable/ic_add_link.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_add_link.xml rename to core/ui/src/main/res/drawable/ic_add_link.xml diff --git a/core/core-ui/src/main/res/drawable/ic_alert.xml b/core/ui/src/main/res/drawable/ic_alert.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_alert.xml rename to core/ui/src/main/res/drawable/ic_alert.xml diff --git a/core/core-ui/src/main/res/drawable/ic_android.xml b/core/ui/src/main/res/drawable/ic_android.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_android.xml rename to core/ui/src/main/res/drawable/ic_android.xml diff --git a/core/core-ui/src/main/res/drawable/ic_android_anim.xml b/core/ui/src/main/res/drawable/ic_android_anim.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_android_anim.xml rename to core/ui/src/main/res/drawable/ic_android_anim.xml diff --git a/core/core-ui/src/main/res/drawable/ic_android_avd.xml b/core/ui/src/main/res/drawable/ic_android_avd.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_android_avd.xml rename to core/ui/src/main/res/drawable/ic_android_avd.xml diff --git a/core/core-ui/src/main/res/drawable/ic_archive.xml b/core/ui/src/main/res/drawable/ic_archive.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_archive.xml rename to core/ui/src/main/res/drawable/ic_archive.xml diff --git a/core/core-ui/src/main/res/drawable/ic_arrow_back.xml b/core/ui/src/main/res/drawable/ic_arrow_back.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_arrow_back.xml rename to core/ui/src/main/res/drawable/ic_arrow_back.xml diff --git a/core/core-ui/src/main/res/drawable/ic_arrow_drop_down.xml b/core/ui/src/main/res/drawable/ic_arrow_drop_down.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_arrow_drop_down.xml rename to core/ui/src/main/res/drawable/ic_arrow_drop_down.xml diff --git a/core/ui/src/main/res/drawable/ic_block.xml b/core/ui/src/main/res/drawable/ic_block.xml new file mode 100644 index 00000000..be2fe7fe --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_block.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/core/core-ui/src/main/res/drawable/ic_bug.xml b/core/ui/src/main/res/drawable/ic_bug.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_bug.xml rename to core/ui/src/main/res/drawable/ic_bug.xml diff --git a/core/core-ui/src/main/res/drawable/ic_bug_anim.xml b/core/ui/src/main/res/drawable/ic_bug_anim.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_bug_anim.xml rename to core/ui/src/main/res/drawable/ic_bug_anim.xml diff --git a/core/core-ui/src/main/res/drawable/ic_bug_avd.xml b/core/ui/src/main/res/drawable/ic_bug_avd.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_bug_avd.xml rename to core/ui/src/main/res/drawable/ic_bug_avd.xml diff --git a/core/core-ui/src/main/res/drawable/ic_bug_notification.xml b/core/ui/src/main/res/drawable/ic_bug_notification.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_bug_notification.xml rename to core/ui/src/main/res/drawable/ic_bug_notification.xml diff --git a/core/ui/src/main/res/drawable/ic_check_circle.xml b/core/ui/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 00000000..9625d649 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/core-ui/src/main/res/drawable/ic_checklist.xml b/core/ui/src/main/res/drawable/ic_checklist.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_checklist.xml rename to core/ui/src/main/res/drawable/ic_checklist.xml diff --git a/core/core-ui/src/main/res/drawable/ic_clear.xml b/core/ui/src/main/res/drawable/ic_clear.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_clear.xml rename to core/ui/src/main/res/drawable/ic_clear.xml diff --git a/core/core-ui/src/main/res/drawable/ic_clear_all.xml b/core/ui/src/main/res/drawable/ic_clear_all.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_clear_all.xml rename to core/ui/src/main/res/drawable/ic_clear_all.xml diff --git a/core/core-ui/src/main/res/drawable/ic_copy.xml b/core/ui/src/main/res/drawable/ic_copy.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_copy.xml rename to core/ui/src/main/res/drawable/ic_copy.xml diff --git a/core/core-ui/src/main/res/drawable/ic_delete.xml b/core/ui/src/main/res/drawable/ic_delete.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_delete.xml rename to core/ui/src/main/res/drawable/ic_delete.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_adb.xml b/core/ui/src/main/res/drawable/ic_dialog_adb.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_adb.xml rename to core/ui/src/main/res/drawable/ic_dialog_adb.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_date_format.xml b/core/ui/src/main/res/drawable/ic_dialog_date_format.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_date_format.xml rename to core/ui/src/main/res/drawable/ic_dialog_date_format.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_eye.xml b/core/ui/src/main/res/drawable/ic_dialog_eye.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_eye.xml rename to core/ui/src/main/res/drawable/ic_dialog_eye.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_list.xml b/core/ui/src/main/res/drawable/ic_dialog_list.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_list.xml rename to core/ui/src/main/res/drawable/ic_dialog_list.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_notification_important.xml b/core/ui/src/main/res/drawable/ic_dialog_notification_important.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_notification_important.xml rename to core/ui/src/main/res/drawable/ic_dialog_notification_important.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_terminal.xml b/core/ui/src/main/res/drawable/ic_dialog_terminal.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_terminal.xml rename to core/ui/src/main/res/drawable/ic_dialog_terminal.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_text_fields.xml b/core/ui/src/main/res/drawable/ic_dialog_text_fields.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_text_fields.xml rename to core/ui/src/main/res/drawable/ic_dialog_text_fields.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_theme.xml b/core/ui/src/main/res/drawable/ic_dialog_theme.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_theme.xml rename to core/ui/src/main/res/drawable/ic_dialog_theme.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_time_format.xml b/core/ui/src/main/res/drawable/ic_dialog_time_format.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_time_format.xml rename to core/ui/src/main/res/drawable/ic_dialog_time_format.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_timer.xml b/core/ui/src/main/res/drawable/ic_dialog_timer.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_timer.xml rename to core/ui/src/main/res/drawable/ic_dialog_timer.xml diff --git a/core/core-ui/src/main/res/drawable/ic_dialog_warning.xml b/core/ui/src/main/res/drawable/ic_dialog_warning.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_dialog_warning.xml rename to core/ui/src/main/res/drawable/ic_dialog_warning.xml diff --git a/core/core-ui/src/main/res/drawable/ic_export.xml b/core/ui/src/main/res/drawable/ic_export.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_export.xml rename to core/ui/src/main/res/drawable/ic_export.xml diff --git a/core/core-ui/src/main/res/drawable/ic_eye.xml b/core/ui/src/main/res/drawable/ic_eye.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_eye.xml rename to core/ui/src/main/res/drawable/ic_eye.xml diff --git a/core/core-ui/src/main/res/drawable/ic_filter.xml b/core/ui/src/main/res/drawable/ic_filter.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_filter.xml rename to core/ui/src/main/res/drawable/ic_filter.xml diff --git a/core/core-ui/src/main/res/drawable/ic_info.xml b/core/ui/src/main/res/drawable/ic_info.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_info.xml rename to core/ui/src/main/res/drawable/ic_info.xml diff --git a/core/core-ui/src/main/res/drawable/ic_launcher_foreground.xml b/core/ui/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_launcher_foreground.xml rename to core/ui/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/core/core-ui/src/main/res/drawable/ic_logfox.xml b/core/ui/src/main/res/drawable/ic_logfox.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_logfox.xml rename to core/ui/src/main/res/drawable/ic_logfox.xml diff --git a/core/core-ui/src/main/res/drawable/ic_logfox_anim.xml b/core/ui/src/main/res/drawable/ic_logfox_anim.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_logfox_anim.xml rename to core/ui/src/main/res/drawable/ic_logfox_anim.xml diff --git a/core/core-ui/src/main/res/drawable/ic_logfox_avd.xml b/core/ui/src/main/res/drawable/ic_logfox_avd.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_logfox_avd.xml rename to core/ui/src/main/res/drawable/ic_logfox_avd.xml diff --git a/core/core-ui/src/main/res/drawable/ic_notifications.xml b/core/ui/src/main/res/drawable/ic_notifications.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_notifications.xml rename to core/ui/src/main/res/drawable/ic_notifications.xml diff --git a/core/core-ui/src/main/res/drawable/ic_pause.xml b/core/ui/src/main/res/drawable/ic_pause.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_pause.xml rename to core/ui/src/main/res/drawable/ic_pause.xml diff --git a/core/core-ui/src/main/res/drawable/ic_play.xml b/core/ui/src/main/res/drawable/ic_play.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_play.xml rename to core/ui/src/main/res/drawable/ic_play.xml diff --git a/core/core-ui/src/main/res/drawable/ic_recording.xml b/core/ui/src/main/res/drawable/ic_recording.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_recording.xml rename to core/ui/src/main/res/drawable/ic_recording.xml diff --git a/core/core-ui/src/main/res/drawable/ic_recording_anim.xml b/core/ui/src/main/res/drawable/ic_recording_anim.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_recording_anim.xml rename to core/ui/src/main/res/drawable/ic_recording_anim.xml diff --git a/core/core-ui/src/main/res/drawable/ic_recording_avd.xml b/core/ui/src/main/res/drawable/ic_recording_avd.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_recording_avd.xml rename to core/ui/src/main/res/drawable/ic_recording_avd.xml diff --git a/core/core-ui/src/main/res/drawable/ic_recording_notification.xml b/core/ui/src/main/res/drawable/ic_recording_notification.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_recording_notification.xml rename to core/ui/src/main/res/drawable/ic_recording_notification.xml diff --git a/core/core-ui/src/main/res/drawable/ic_recording_play_notification.xml b/core/ui/src/main/res/drawable/ic_recording_play_notification.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_recording_play_notification.xml rename to core/ui/src/main/res/drawable/ic_recording_play_notification.xml diff --git a/core/core-ui/src/main/res/drawable/ic_save.xml b/core/ui/src/main/res/drawable/ic_save.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_save.xml rename to core/ui/src/main/res/drawable/ic_save.xml diff --git a/core/core-ui/src/main/res/drawable/ic_search.xml b/core/ui/src/main/res/drawable/ic_search.xml similarity index 90% rename from core/core-ui/src/main/res/drawable/ic_search.xml rename to core/ui/src/main/res/drawable/ic_search.xml index f9e527d9..ac752f9b 100644 --- a/core/core-ui/src/main/res/drawable/ic_search.xml +++ b/core/ui/src/main/res/drawable/ic_search.xml @@ -1,9 +1,10 @@ diff --git a/core/core-ui/src/main/res/drawable/ic_select.xml b/core/ui/src/main/res/drawable/ic_select.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_select.xml rename to core/ui/src/main/res/drawable/ic_select.xml diff --git a/core/core-ui/src/main/res/drawable/ic_select_all.xml b/core/ui/src/main/res/drawable/ic_select_all.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_select_all.xml rename to core/ui/src/main/res/drawable/ic_select_all.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings.xml b/core/ui/src/main/res/drawable/ic_settings.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings.xml rename to core/ui/src/main/res/drawable/ic_settings.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_anim.xml b/core/ui/src/main/res/drawable/ic_settings_anim.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_anim.xml rename to core/ui/src/main/res/drawable/ic_settings_anim.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_avd.xml b/core/ui/src/main/res/drawable/ic_settings_avd.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_avd.xml rename to core/ui/src/main/res/drawable/ic_settings_avd.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_code.xml b/core/ui/src/main/res/drawable/ic_settings_code.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_code.xml rename to core/ui/src/main/res/drawable/ic_settings_code.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_crashes.xml b/core/ui/src/main/res/drawable/ic_settings_crashes.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_crashes.xml rename to core/ui/src/main/res/drawable/ic_settings_crashes.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_handyman.xml b/core/ui/src/main/res/drawable/ic_settings_handyman.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_handyman.xml rename to core/ui/src/main/res/drawable/ic_settings_handyman.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_info.xml b/core/ui/src/main/res/drawable/ic_settings_info.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_info.xml rename to core/ui/src/main/res/drawable/ic_settings_info.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_notifications.xml b/core/ui/src/main/res/drawable/ic_settings_notifications.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_notifications.xml rename to core/ui/src/main/res/drawable/ic_settings_notifications.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_person.xml b/core/ui/src/main/res/drawable/ic_settings_person.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_person.xml rename to core/ui/src/main/res/drawable/ic_settings_person.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_releases.xml b/core/ui/src/main/res/drawable/ic_settings_releases.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_releases.xml rename to core/ui/src/main/res/drawable/ic_settings_releases.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_service.xml b/core/ui/src/main/res/drawable/ic_settings_service.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_service.xml rename to core/ui/src/main/res/drawable/ic_settings_service.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_ui.xml b/core/ui/src/main/res/drawable/ic_settings_ui.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_ui.xml rename to core/ui/src/main/res/drawable/ic_settings_ui.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_users.xml b/core/ui/src/main/res/drawable/ic_settings_users.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_users.xml rename to core/ui/src/main/res/drawable/ic_settings_users.xml diff --git a/core/core-ui/src/main/res/drawable/ic_settings_warning.xml b/core/ui/src/main/res/drawable/ic_settings_warning.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_settings_warning.xml rename to core/ui/src/main/res/drawable/ic_settings_warning.xml diff --git a/core/core-ui/src/main/res/drawable/ic_share.xml b/core/ui/src/main/res/drawable/ic_share.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_share.xml rename to core/ui/src/main/res/drawable/ic_share.xml diff --git a/core/core-ui/src/main/res/drawable/ic_sort.xml b/core/ui/src/main/res/drawable/ic_sort.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_sort.xml rename to core/ui/src/main/res/drawable/ic_sort.xml diff --git a/core/core-ui/src/main/res/drawable/ic_square_root.xml b/core/ui/src/main/res/drawable/ic_square_root.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_square_root.xml rename to core/ui/src/main/res/drawable/ic_square_root.xml diff --git a/core/core-ui/src/main/res/drawable/ic_stop.xml b/core/ui/src/main/res/drawable/ic_stop.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_stop.xml rename to core/ui/src/main/res/drawable/ic_stop.xml diff --git a/core/core-ui/src/main/res/drawable/ic_terminal.xml b/core/ui/src/main/res/drawable/ic_terminal.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/ic_terminal.xml rename to core/ui/src/main/res/drawable/ic_terminal.xml diff --git a/core/core-ui/src/main/res/drawable/item_log_background.xml b/core/ui/src/main/res/drawable/item_log_background.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/item_log_background.xml rename to core/ui/src/main/res/drawable/item_log_background.xml diff --git a/core/core-ui/src/main/res/drawable/item_log_level_background.xml b/core/ui/src/main/res/drawable/item_log_level_background.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/item_log_level_background.xml rename to core/ui/src/main/res/drawable/item_log_level_background.xml diff --git a/core/core-ui/src/main/res/drawable/placeholder_icon_background.xml b/core/ui/src/main/res/drawable/placeholder_icon_background.xml similarity index 100% rename from core/core-ui/src/main/res/drawable/placeholder_icon_background.xml rename to core/ui/src/main/res/drawable/placeholder_icon_background.xml diff --git a/core/core-ui/src/main/res/font/google_sans.ttf b/core/ui/src/main/res/font/google_sans.ttf similarity index 100% rename from core/core-ui/src/main/res/font/google_sans.ttf rename to core/ui/src/main/res/font/google_sans.ttf diff --git a/core/core-ui/src/main/res/font/google_sans_medium.ttf b/core/ui/src/main/res/font/google_sans_medium.ttf similarity index 100% rename from core/core-ui/src/main/res/font/google_sans_medium.ttf rename to core/ui/src/main/res/font/google_sans_medium.ttf diff --git a/core/core-ui/src/main/res/layout/dialog_text.xml b/core/ui/src/main/res/layout/dialog_text.xml similarity index 100% rename from core/core-ui/src/main/res/layout/dialog_text.xml rename to core/ui/src/main/res/layout/dialog_text.xml diff --git a/core/core-ui/src/main/res/layout/fragment_settings.xml b/core/ui/src/main/res/layout/fragment_settings.xml similarity index 100% rename from core/core-ui/src/main/res/layout/fragment_settings.xml rename to core/ui/src/main/res/layout/fragment_settings.xml diff --git a/core/core-ui/src/main/res/values-night/colors.xml b/core/ui/src/main/res/values-night/colors.xml similarity index 100% rename from core/core-ui/src/main/res/values-night/colors.xml rename to core/ui/src/main/res/values-night/colors.xml diff --git a/core/core-ui/src/main/res/values/colors.xml b/core/ui/src/main/res/values/colors.xml similarity index 100% rename from core/core-ui/src/main/res/values/colors.xml rename to core/ui/src/main/res/values/colors.xml diff --git a/core/core-ui/src/main/res/values/ids.xml b/core/ui/src/main/res/values/ids.xml similarity index 100% rename from core/core-ui/src/main/res/values/ids.xml rename to core/ui/src/main/res/values/ids.xml diff --git a/core/core-ui/src/main/res/values/styles.xml b/core/ui/src/main/res/values/styles.xml similarity index 98% rename from core/core-ui/src/main/res/values/styles.xml rename to core/ui/src/main/res/values/styles.xml index 7d32dee7..9f87fe53 100644 --- a/core/core-ui/src/main/res/values/styles.xml +++ b/core/ui/src/main/res/values/styles.xml @@ -66,5 +66,6 @@ diff --git a/core/core-ui/src/main/res/values/themes.xml b/core/ui/src/main/res/values/themes.xml similarity index 100% rename from core/core-ui/src/main/res/values/themes.xml rename to core/ui/src/main/res/values/themes.xml diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt b/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt index 118278f7..b77e3ea0 100644 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt +++ b/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt @@ -1,7 +1,7 @@ package com.f0x1d.logfox.model data class InstalledApp( - val title: CharSequence, + val title: String, val packageName: String, ) : Identifiable { override val id: Any get() = packageName diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/event/Event.kt b/data/src/main/kotlin/com/f0x1d/logfox/model/event/Event.kt deleted file mode 100644 index 09011a54..00000000 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/event/Event.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.f0x1d.logfox.model.event - -open class Event( - open val type: String, - private val data: Any -) { - var isConsumed = false - protected set - - fun consume(): T? { - if (isConsumed) - return null - - isConsumed = true - return data as T - } -} diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/event/NoDataEvent.kt b/data/src/main/kotlin/com/f0x1d/logfox/model/event/NoDataEvent.kt deleted file mode 100644 index 625003d3..00000000 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/event/NoDataEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.f0x1d.logfox.model.event - -class NoDataEvent(type: String): Event(type, Unit) { - - override val type: String - get() { - isConsumed = true - return super.type - } -} diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/event/SnackbarEvent.kt b/data/src/main/kotlin/com/f0x1d/logfox/model/event/SnackbarEvent.kt deleted file mode 100644 index d4ea81df..00000000 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/event/SnackbarEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.f0x1d.logfox.model.event - -class SnackbarEvent(text: String): Event(TYPE, text) { - companion object { - const val TYPE = "snackbar" - } -} diff --git a/feature/feature-crashes/.gitignore b/feature/apps-picker/.gitignore similarity index 100% rename from feature/feature-crashes/.gitignore rename to feature/apps-picker/.gitignore diff --git a/feature/apps-picker/build.gradle.kts b/feature/apps-picker/build.gradle.kts new file mode 100644 index 00000000..e215e003 --- /dev/null +++ b/feature/apps-picker/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("logfox.android.feature.compose") +} + +android.namespace = "com.f0x1d.logfox.feature.apps.picker" + +dependencies { + implementation(libs.coil.compose) +} diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt new file mode 100644 index 00000000..2234fe25 --- /dev/null +++ b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt @@ -0,0 +1,68 @@ +package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.f0x1d.logfox.arch.ui.fragment.compose.BaseComposeViewModelFragment +import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenContent +import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenListener +import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenState +import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerViewModel +import com.f0x1d.logfox.feature.apps.picker.viewmodel.resultHandler +import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.collections.immutable.toImmutableSet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +@AndroidEntryPoint +class AppsPickerFragment: BaseComposeViewModelFragment() { + + override val viewModel by viewModels() + private val resultHandler by resultHandler() + + private val listener by lazy { + AppsPickerScreenListener( + onBackClicked = { + viewModel.performBackAction(findNavController()::popBackStack) + }, + onAppClicked = { + if (resultHandler?.onAppSelected(it) == true) { + findNavController().popBackStack() + } + }, + onAppChecked = { app, checked -> resultHandler?.onAppChecked(app, checked) }, + onSearchActiveChanged = viewModel::changeSearchActive, + onQueryChanged = viewModel::updateQuery, + ) + } + + private val uiState: Flow by lazy { + resultHandler?.let { handler -> + combine(viewModel.uiState, handler.checkedAppPackageNames) { state, checkedApps -> + state to checkedApps + }.map { (state, checkedAppPackageNames) -> + state.copy( + topBarTitle = handler.providePickerTopAppBarTitle(requireContext()), + checkedAppPackageNames = checkedAppPackageNames.toImmutableSet(), + multiplySelectionEnabled = handler.supportsMultiplySelection, + ) + } + } ?: viewModel.uiState + } + + @Composable + override fun Content() { + LogFoxTheme { + val state by uiState.collectAsState(initial = viewModel.currentState) + + AppsPickerScreenContent( + state = state, + listener = listener, + ) + } + } +} diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt new file mode 100644 index 00000000..47ce9044 --- /dev/null +++ b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt @@ -0,0 +1,250 @@ +package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.f0x1d.logfox.model.InstalledApp +import com.f0x1d.logfox.strings.Strings +import com.f0x1d.logfox.ui.compose.component.button.NavigationBackButton +import com.f0x1d.logfox.ui.compose.component.search.TopSearchBar +import com.f0x1d.logfox.ui.compose.preview.DayNightPreview +import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf + +@Composable +internal fun AppsPickerScreenContent( + state: AppsPickerScreenState = AppsPickerScreenState(), + listener: AppsPickerScreenListener = MockAppsPickerScreenListener, +) { + CompositionLocalProvider(LocalMultiplySelectionEnabled provides state.multiplySelectionEnabled) { + Scaffold( + topBar = { + AppsSearchBar( + state = state, + listener = listener, + ) + }, + ) { paddingValues -> + if (state.isLoading) { + LoadingContent(modifier = Modifier.padding(paddingValues)) + } else { + AppsContent( + items = state.apps, + checkedItems = state.checkedAppPackageNames, + listener = listener, + contentPadding = paddingValues, + ) + } + } + + BackHandler( + enabled = state.searchActive, + onBack = listener.onBackClicked, + ) + } +} + +@Composable +private fun AppsSearchBar( + state: AppsPickerScreenState, + listener: AppsPickerScreenListener, + modifier: Modifier = Modifier, +) { + TopSearchBar( + modifier = modifier, + query = state.query, + onQueryChange = listener.onQueryChanged, + onSearch = { /* noop */ }, + active = state.searchActive, + onActiveChange = listener.onSearchActiveChanged, + placeholder = { + Text( + text = if (state.searchActive) { + stringResource(id = Strings.apps) + } else { + state.topBarTitle + }, + ) + }, + leadingIcon = { NavigationBackButton(onClick = listener.onBackClicked) }, + ) { + AppsContent( + items = state.searchedApps, + checkedItems = state.checkedAppPackageNames, + listener = listener, + contentPadding = WindowInsets.navigationBars + .union(WindowInsets.ime) + .asPaddingValues(), + ) + } +} + +@Composable +private fun LoadingContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AppsContent( + items: ImmutableList, + checkedItems: ImmutableSet, + listener: AppsPickerScreenListener, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = contentPadding, + ) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item.javaClass }, + ) { index, item -> + Column(modifier = Modifier.animateItemPlacement()) { + AppContent( + item = item, + isChecked = remember(checkedItems) { + item.packageName in checkedItems + }, + onClick = listener.onAppClicked, + onChecked = listener.onAppChecked, + ) + + if (index != items.lastIndex) { + HorizontalDivider( + modifier = Modifier.padding( + start = 80.dp, + end = 10.dp, + ) + ) + } + } + } + } +} + +@Composable +internal fun AppContent( + item: InstalledApp, + isChecked: Boolean, + onClick: (InstalledApp) -> Unit, + onChecked: (InstalledApp, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(85.dp) + .clickable { onClick(item) } + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + AsyncImage( + modifier = Modifier.size(60.dp), + model = item, + contentDescription = null, + ) + + Column( + modifier = Modifier + .weight(1f) + .height(60.dp), + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = item.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = item.packageName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (LocalMultiplySelectionEnabled.current) { + Checkbox( + checked = isChecked, + onCheckedChange = { onChecked(item, it) }, + ) + } + } +} + +internal val MockApps = persistentListOf( + InstalledApp("LogFox", "com.f0x1d.logfox"), + InstalledApp("Sense", "com.f0x1d.sense"), +) +internal val MockAppsPickerScreenState = AppsPickerScreenState( + apps = MockApps, + searchedApps = MockApps, + checkedAppPackageNames = persistentSetOf(MockApps.first().packageName), + isLoading = false, +) + +@DayNightPreview +@Composable +private fun AppsPickerScreenContentPreview() = LogFoxTheme { + AppsPickerScreenContent( + state = MockAppsPickerScreenState, + ) +} + +@DayNightPreview +@Composable +private fun AppsPickerSearchScreenContentPreview() = LogFoxTheme { + AppsPickerScreenContent( + state = MockAppsPickerScreenState.copy(searchActive = true), + ) +} + +private val LocalMultiplySelectionEnabled = compositionLocalOf { false } diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt new file mode 100644 index 00000000..71ea5f39 --- /dev/null +++ b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt @@ -0,0 +1,34 @@ +package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose + +import com.f0x1d.logfox.model.InstalledApp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf + +data class AppsPickerScreenState( + val topBarTitle: String = "Apps", + val apps: ImmutableList = persistentListOf(), + val checkedAppPackageNames: ImmutableSet = persistentSetOf(), + val searchedApps: ImmutableList = persistentListOf(), + val multiplySelectionEnabled: Boolean = true, + val isLoading: Boolean = true, + val searchActive: Boolean = false, + val query: String = "", +) + +data class AppsPickerScreenListener( + val onBackClicked: () -> Unit, + val onAppClicked: (InstalledApp) -> Unit, + val onAppChecked: (InstalledApp, Boolean) -> Unit, + val onSearchActiveChanged: (Boolean) -> Unit, + val onQueryChanged: (String) -> Unit, +) + +internal val MockAppsPickerScreenListener = AppsPickerScreenListener( + onBackClicked = { }, + onAppClicked = { }, + onAppChecked = { _, _ -> }, + onSearchActiveChanged = { }, + onQueryChanged = { }, +) diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt new file mode 100644 index 00000000..819b36b3 --- /dev/null +++ b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt @@ -0,0 +1,34 @@ +package com.f0x1d.logfox.feature.apps.picker.viewmodel + +import android.annotation.SuppressLint +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.f0x1d.logfox.model.InstalledApp +import com.f0x1d.logfox.strings.Strings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +interface AppsPickerResultHandler { + val supportsMultiplySelection: Boolean get() = false + val checkedAppPackageNames: Flow> get() = flowOf(emptySet()) + + fun providePickerTopAppBarTitle(context: Context) = context.getString(Strings.apps) + + fun onAppChecked(app: InstalledApp, checked: Boolean) = Unit + + // pass true to close fragment + fun onAppSelected(app: InstalledApp): Boolean = false +} + +@SuppressLint("RestrictedApi") +internal fun Fragment.resultHandler(): Lazy = lazy { + val backStackEntry = findNavController().previousBackStackEntry + ?: return@lazy null + + val store = backStackEntry.viewModelStore + val availableViewModelKeys = store.keys() + + availableViewModelKeys + .firstNotNullOfOrNull { store[it] as? AppsPickerResultHandler } +} diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt new file mode 100644 index 00000000..ddbca2a3 --- /dev/null +++ b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt @@ -0,0 +1,80 @@ +package com.f0x1d.logfox.feature.apps.picker.viewmodel + +import android.app.Application +import com.f0x1d.logfox.arch.di.DefaultDispatcher +import com.f0x1d.logfox.arch.viewmodel.BaseStateViewModel +import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenState +import com.f0x1d.logfox.model.InstalledApp +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class AppsPickerViewModel @Inject constructor( + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + application: Application, +): BaseStateViewModel( + initialStateProvider = { AppsPickerScreenState() }, + application = application, +) { + + private val query = MutableStateFlow("") + + init { + load() + } + + fun performBackAction(popBackStack: () -> Unit) = state { + if (searchActive) { + copy(searchActive = false) + } else { + popBackStack() + this + } + } + + fun changeSearchActive(active: Boolean) = state { + copy(searchActive = active) + } + + fun updateQuery(text: String) = state { + copy(query = text) + }.also { + query.update { text } + } + + private fun load() = launchCatching(defaultDispatcher) { + val packageManager = ctx.packageManager + + val installedApps = packageManager.getInstalledPackages(0).map { + InstalledApp( + title = it.applicationInfo.loadLabel(packageManager).toString(), + packageName = it.packageName, + ) + }.sortedBy(InstalledApp::title) + + state { + copy( + apps = installedApps.toImmutableList(), + isLoading = false, + ) + } + + query.map { query -> + installedApps.filter { app -> + app.title.contains(query, ignoreCase = true) + || app.packageName.contains(query, ignoreCase = true) + } + }.flowOn( + defaultDispatcher, + ).collectLatest { apps -> + state { copy(searchedApps = apps.toImmutableList()) } + } + } +} diff --git a/core/core-terminals/.gitignore b/feature/crashes-core/.gitignore similarity index 100% rename from core/core-terminals/.gitignore rename to feature/crashes-core/.gitignore diff --git a/feature/feature-crashes-core/build.gradle.kts b/feature/crashes-core/build.gradle.kts similarity index 100% rename from feature/feature-crashes-core/build.gradle.kts rename to feature/crashes-core/build.gradle.kts diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt similarity index 60% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt index be7a3288..4d7b90d1 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt @@ -2,8 +2,9 @@ package com.f0x1d.logfox.feature.crashes.core.controller import android.content.Context import com.f0x1d.logfox.arch.di.IODispatcher -import com.f0x1d.logfox.database.AppDatabase import com.f0x1d.logfox.database.entity.AppCrash +import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository +import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository import com.f0x1d.logfox.feature.crashes.core.repository.reader.ANRDetector import com.f0x1d.logfox.feature.crashes.core.repository.reader.JNICrashDetector import com.f0x1d.logfox.feature.crashes.core.repository.reader.JavaCrashDetector @@ -22,7 +23,8 @@ interface CrashesController { internal class CrashesControllerImpl @Inject constructor( @ApplicationContext private val context: Context, private val notificationsController: CrashesNotificationsController, - private val database: AppDatabase, + private val crashesRepository: CrashesRepository, + private val disabledAppsRepository: DisabledAppsRepository, private val appPreferences: AppPreferences, @IODispatcher private val ioDispatcher: CoroutineDispatcher, ) : CrashesController { @@ -40,42 +42,48 @@ internal class CrashesControllerImpl @Inject constructor( ANRDetector(context, this::collectCrash), ) - private suspend fun collectCrash(it: AppCrash, lines: List) = withContext(ioDispatcher) { + private suspend fun collectCrash(appCrash: AppCrash, lines: List) { + if (disabledAppsRepository.isDisabledFor(appCrash.packageName)) { + return + } + // Don't handle if already present in data - database.appCrashes().getAllByDateAndTime( - dateAndTime = it.dateAndTime, - packageName = it.packageName + crashesRepository.getAllByDateAndTime( + dateAndTime = appCrash.dateAndTime, + packageName = appCrash.packageName ).also { - if (it.isNotEmpty()) return@withContext + if (it.isNotEmpty()) return } val crashLog = lines.joinToString("\n") { it.content } - val sendNotificationIfNeeded = { appCrash: AppCrash -> - if (appPreferences.showingNotificationsFor(appCrash.crashType)) { - notificationsController.sendErrorNotification(appCrash, crashLog) + val sendNotificationIfNeeded = { crash: AppCrash -> + if (appPreferences.showingNotificationsFor(crash.crashType)) { + notificationsController.sendErrorNotification(crash, crashLog) } } - val logFile = File(logsDir, "${it.dateAndTime}-crash.log").apply { - writeText(crashLog) + val logFile = withContext(ioDispatcher) { + File(logsDir, "${appCrash.dateAndTime}-crash.log").apply { + writeText(crashLog) + } } - val appCrash = it.copy( + val appCrashWithLog = appCrash.copy( logFile = logFile, logDumpFile = null, // TODO: return log dumps! ) - if (appPreferences.collectingFor(appCrash.crashType)) { - val appCrashWithId = appCrash.copy( - id = database.appCrashes().insert(appCrash) + if (appPreferences.collectingFor(appCrashWithLog.crashType)) { + val appCrashWithId = appCrashWithLog.copy( + id = crashesRepository.insert(appCrashWithLog), ) sendNotificationIfNeeded(appCrashWithId) } else { - sendNotificationIfNeeded(appCrash) + sendNotificationIfNeeded(appCrashWithLog) } } } diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt similarity index 93% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt index f7d58469..bf01a755 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt @@ -9,14 +9,14 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.os.bundleOf import androidx.navigation.NavDeepLinkBuilder -import com.f0x1d.logfox.context.CRASHES_CHANNEL_GROUP_ID -import com.f0x1d.logfox.context.doIfNotificationsAllowed -import com.f0x1d.logfox.context.notificationManager -import com.f0x1d.logfox.context.notificationManagerCompat -import com.f0x1d.logfox.context.receiver.CopyReceiver +import com.f0x1d.logfox.arch.COPY_CRASH_INTENT_ID +import com.f0x1d.logfox.arch.CRASHES_CHANNEL_GROUP_ID +import com.f0x1d.logfox.arch.doIfNotificationsAllowed +import com.f0x1d.logfox.arch.makeBroadcastPendingIntent +import com.f0x1d.logfox.arch.notificationManager +import com.f0x1d.logfox.arch.notificationManagerCompat +import com.f0x1d.logfox.arch.receiver.CopyReceiver import com.f0x1d.logfox.database.entity.AppCrash -import com.f0x1d.logfox.intents.COPY_CRASH_INTENT_ID -import com.f0x1d.logfox.intents.makeBroadcastPendingIntent import com.f0x1d.logfox.navigation.Directions import com.f0x1d.logfox.navigation.NavGraphs import com.f0x1d.logfox.preferences.shared.AppPreferences diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt similarity index 63% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt index 8cac7abe..7249bba7 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt @@ -2,6 +2,8 @@ package com.f0x1d.logfox.feature.crashes.core.di import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepositoryImpl +import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository +import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +17,9 @@ internal interface RepositoriesModule { fun bindCrashesRepository( crashesRepositoryImpl: CrashesRepositoryImpl, ): CrashesRepository + + @Binds + fun provideDisabledAppsRepository( + disabledAppsRepositoryImpl: DisabledAppsRepositoryImpl, + ): DisabledAppsRepository } diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt similarity index 75% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt index 32cfe0c3..e8119600 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt @@ -7,11 +7,18 @@ import com.f0x1d.logfox.database.entity.AppCrash import com.f0x1d.logfox.feature.crashes.core.controller.CrashesNotificationsController import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import javax.inject.Inject interface CrashesRepository : DatabaseProxyRepository { + suspend fun getAllByDateAndTime( + dateAndTime: Long, + packageName: String, + ): List + suspend fun insert(appCrash: AppCrash): Long + suspend fun deleteAllByPackageName(appCrash: AppCrash) } @@ -22,7 +29,9 @@ internal class CrashesRepositoryImpl @Inject constructor( ) : CrashesRepository { override fun getAllAsFlow(): Flow> = - database.appCrashes().getAllAsFlow().flowOn(ioDispatcher) + database.appCrashes().getAllAsFlow() + .distinctUntilChanged() + .flowOn(ioDispatcher) override fun getByIdAsFlow(id: Long): Flow = database.appCrashes().getByIdAsFlow(id).flowOn(ioDispatcher) @@ -35,6 +44,20 @@ internal class CrashesRepositoryImpl @Inject constructor( database.appCrashes().getById(id) } + override suspend fun getAllByDateAndTime( + dateAndTime: Long, + packageName: String + ): List = withContext(ioDispatcher) { + database.appCrashes().getAllByDateAndTime( + dateAndTime = dateAndTime, + packageName = packageName, + ) + } + + override suspend fun insert(appCrash: AppCrash): Long = withContext(ioDispatcher) { + database.appCrashes().insert(appCrash) + } + override suspend fun deleteAllByPackageName(appCrash: AppCrash) = withContext(ioDispatcher) { database.appCrashes().getAllByPackageName(appCrash.packageName).forEach { it.deleteAssociatedFiles() diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt new file mode 100644 index 00000000..2bf10270 --- /dev/null +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt @@ -0,0 +1,80 @@ +package com.f0x1d.logfox.feature.crashes.core.repository + +import com.f0x1d.logfox.arch.di.IODispatcher +import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository +import com.f0x1d.logfox.database.AppDatabase +import com.f0x1d.logfox.database.entity.DisabledApp +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface DisabledAppsRepository : DatabaseProxyRepository { + suspend fun isDisabledFor(packageName: String): Boolean + fun disabledForFlow(packageName: String): Flow + + suspend fun checkApp(packageName: String) + suspend fun checkApp(packageName: String, checked: Boolean) +} + +internal class DisabledAppsRepositoryImpl @Inject constructor( + private val database: AppDatabase, + @IODispatcher private val ioDispatcher: CoroutineDispatcher, +) : DisabledAppsRepository { + + override suspend fun isDisabledFor(packageName: String): Boolean = withContext(ioDispatcher) { + database.disabledApps().getByPackageName(packageName) != null + } + + override fun disabledForFlow(packageName: String): Flow = + database.disabledApps() + .getByPackageNameAsFlow(packageName) + .map { it != null } + .flowOn(ioDispatcher) + + override suspend fun checkApp(packageName: String) = checkApp( + packageName = packageName, + checked = withContext(ioDispatcher) { + database.disabledApps().getByPackageName(packageName) == null + }, + ) + + override suspend fun checkApp(packageName: String, checked: Boolean) = withContext(ioDispatcher) { + if (checked) { + database.disabledApps().insert(DisabledApp(packageName)) + } else { + database.disabledApps().deleteByPackageName(packageName) + } + } + + override fun getAllAsFlow(): Flow> = + database.disabledApps().getAllAsFlow() + .distinctUntilChanged() + .flowOn(ioDispatcher) + + override fun getByIdAsFlow(id: Long): Flow = + database.disabledApps().getByIdAsFlow(id).flowOn(ioDispatcher) + + override suspend fun getAll(): List = withContext(ioDispatcher) { + database.disabledApps().getAll() + } + + override suspend fun getById(id: Long): DisabledApp? = withContext(ioDispatcher) { + database.disabledApps().getById(id) + } + + override suspend fun update(item: DisabledApp) = withContext(ioDispatcher) { + database.disabledApps().update(item) + } + + override suspend fun delete(item: DisabledApp) = withContext(ioDispatcher) { + database.disabledApps().delete(item) + } + + override suspend fun clear() = withContext(ioDispatcher) { + database.disabledApps().deleteAll() + } +} diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt index 2498b1f6..563fc52d 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt +++ b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt @@ -3,8 +3,8 @@ package com.f0x1d.logfox.feature.crashes.core.repository.reader import android.content.Context import com.f0x1d.logfox.database.entity.AppCrash import com.f0x1d.logfox.database.entity.CrashType -import com.f0x1d.logfox.model.logline.LogLine import com.f0x1d.logfox.feature.crashes.core.repository.reader.base.BaseCrashDetector +import com.f0x1d.logfox.model.logline.LogLine internal class JavaCrashDetector( context: Context, diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt b/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt similarity index 100% rename from feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt rename to feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt diff --git a/feature/feature-logging/.gitignore b/feature/crashes/.gitignore similarity index 100% rename from feature/feature-logging/.gitignore rename to feature/crashes/.gitignore diff --git a/feature/feature-crashes/build.gradle.kts b/feature/crashes/build.gradle.kts similarity index 60% rename from feature/feature-crashes/build.gradle.kts rename to feature/crashes/build.gradle.kts index 070c49ab..520137cc 100644 --- a/feature/feature-crashes/build.gradle.kts +++ b/feature/crashes/build.gradle.kts @@ -5,8 +5,8 @@ plugins { android.namespace = "com.f0x1d.logfox.feature.crashes" dependencies { - implementation(project(":feature:feature-crashes-core")) + implementation(projects.feature.appsPicker) + implementation(projects.feature.crashesCore) implementation(libs.glide) - ksp(libs.glide.compiler) } diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt similarity index 100% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt similarity index 100% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt similarity index 100% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt new file mode 100644 index 00000000..a11af621 --- /dev/null +++ b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt @@ -0,0 +1,217 @@ +package com.f0x1d.logfox.feature.crashes.ui.fragment + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.text.Spannable +import android.text.style.BackgroundColorSpan +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.SearchView +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.f0x1d.logfox.arch.copyText +import com.f0x1d.logfox.arch.notificationsChannelsAvailable +import com.f0x1d.logfox.arch.shareIntent +import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment +import com.f0x1d.logfox.database.entity.AppCrash +import com.f0x1d.logfox.feature.crashes.R +import com.f0x1d.logfox.feature.crashes.core.controller.notificationChannelId +import com.f0x1d.logfox.feature.crashes.databinding.FragmentCrashDetailsBinding +import com.f0x1d.logfox.feature.crashes.viewmodel.CrashDetailsViewModel +import com.f0x1d.logfox.strings.Strings +import com.f0x1d.logfox.ui.Colors +import com.f0x1d.logfox.ui.Icons +import com.f0x1d.logfox.ui.dialog.showAreYouSureDeleteDialog +import com.f0x1d.logfox.ui.dialog.showAreYouSureDialog +import com.f0x1d.logfox.ui.view.loadIcon +import com.f0x1d.logfox.ui.view.setClickListenerOn +import com.f0x1d.logfox.ui.view.setupBackButtonForNavController +import dagger.hilt.android.AndroidEntryPoint +import dev.chrisbanes.insetter.applyInsetter +import java.util.Locale + +@AndroidEntryPoint +class CrashDetailsFragment: BaseViewModelFragment() { + + override val viewModel by viewModels() + + private val zipCrashLauncher = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip"), + ) { + viewModel.exportCrashToZip(it ?: return@registerForActivityResult) + } + + private val closeSearchOnBackPressedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + binding.searchItem.collapseActionView() + } + } + + override fun inflateBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ) = FragmentCrashDetailsBinding.inflate(inflater, container, false) + + override fun FragmentCrashDetailsBinding.onViewCreated(view: View, savedInstanceState: Bundle?) { + scrollView.applyInsetter { + type(navigationBars = true) { + padding(vertical = true) + } + } + + toolbar.setupBackButtonForNavController() + toolbar.menu.apply { + findItem(R.id.notifications_item).setVisible( + notificationsChannelsAvailable + && viewModel.useSeparateNotificationsChannelsForCrashes + ) + } + searchItem.setOnActionExpandListener( + object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + closeSearchOnBackPressedCallback.isEnabled = false + return true + } + + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + closeSearchOnBackPressedCallback.isEnabled = true + return true + } + } + ) + (searchItem.actionView as SearchView).setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean = true + + override fun onQueryTextChange(newText: String?): Boolean { + searchInLog(newText ?: return false) + return true + } + } + ) + + viewModel.crash.collectWithLifecycle { + setupFor(it ?: return@collectWithLifecycle) + } + + viewModel.blacklisted.collectWithLifecycle { blacklisted -> + toolbar.menu.findItem(R.id.blacklist_item).apply { + if (blacklisted == null) { + isVisible = false + } else { + isVisible = true + setIcon(if (blacklisted) Icons.ic_check_circle else Icons.ic_block) + setTitle(if (blacklisted) Strings.remove_from_blacklist else Strings.add_to_blacklist) + } + } + } + + requireActivity().onBackPressedDispatcher.apply { + addCallback(viewLifecycleOwner, closeSearchOnBackPressedCallback) + } + } + + private fun FragmentCrashDetailsBinding.searchInLog(text: String) { + var stackTrace = viewModel.crash.value?.second ?: return + var query = text + + val span = stackTrace.toSpannable() + if (query.isNotEmpty()) { + query = query.lowercase(Locale.ENGLISH) + stackTrace = stackTrace.lowercase(Locale.ENGLISH) + + val size = query.length + var index = 0 + val highlightColor = requireContext().getColor(Colors.md_theme_primaryContainer) + while (stackTrace.indexOf(query, index).also { index = it } != -1) { + span.setSpan( + BackgroundColorSpan(highlightColor), + index, + index + size, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + index += size + } + } + logText.setText(span, TextView.BufferType.SPANNABLE) + logTextScrollable.setText(span, TextView.BufferType.SPANNABLE) + } + + @SuppressLint("InlinedApi") + private fun FragmentCrashDetailsBinding.setupFor(item: Pair) { + val (appCrash, crashLog) = item + + toolbar.menu.apply { + setClickListenerOn(R.id.info_item) { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", appCrash.packageName, null) + }.let(::startActivity) + } + setClickListenerOn(R.id.notifications_item) { + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, appCrash.notificationChannelId) + }.let(::startActivity) + } + setClickListenerOn(R.id.blacklist_item) { + if (viewModel.blacklisted.value == false) { + showAreYouSureDialog( + title = Strings.blacklist, + message = Strings.warning_blacklist, + ) { + viewModel.changeBlacklist(appCrash) + } + } else { + viewModel.changeBlacklist(appCrash) + } + } + setClickListenerOn(R.id.delete_item) { + showAreYouSureDeleteDialog { + viewModel.deleteCrash(appCrash) + findNavController().popBackStack() + } + } + } + + appLogo.loadIcon(appCrash.packageName) + appName.text = appCrash.appName ?: getString(Strings.unknown) + appPackage.text = appCrash.packageName + + copyButton.setOnClickListener { + requireContext().copyText(crashLog ?: "") + snackbar(Strings.text_copied) + } + + shareButton.setOnClickListener { + requireContext().shareIntent(crashLog ?: "") + } + + zipButton.setOnClickListener { + val pkg = appCrash.packageName.replace(".", "-") + val formattedDate = viewModel.formatForExport(appCrash.dateAndTime) + + zipCrashLauncher.launch("crash-$pkg-$formattedDate.zip") + } + + viewModel.wrapCrashLogLines.let { wrap -> + logText.isVisible = wrap + logTextScrollableContainer.isVisible = wrap.not() + } + + logText.text = crashLog + logTextScrollable.text = crashLog + } + + private val FragmentCrashDetailsBinding.searchItem get() = + toolbar.menu.findItem(R.id.search_item) +} diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt similarity index 100% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt similarity index 94% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt index 83b6b18c..d2e102ba 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt +++ b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt @@ -8,11 +8,11 @@ import androidx.activity.OnBackPressedCallback import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.viewModels +import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager +import com.f0x1d.logfox.arch.isHorizontalOrientation import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment -import com.f0x1d.logfox.context.isHorizontalOrientation import com.f0x1d.logfox.feature.crashes.R import com.f0x1d.logfox.feature.crashes.adapter.CrashesAdapter import com.f0x1d.logfox.feature.crashes.databinding.DialogSortingBinding @@ -35,7 +35,7 @@ import dev.chrisbanes.insetter.applyInsetter @AndroidEntryPoint class CrashesFragment: BaseViewModelFragment() { - override val viewModel by viewModels() + override val viewModel by hiltNavGraphViewModels(Directions.crashesFragment) private val adapter = CrashesAdapter( click = { @@ -104,6 +104,9 @@ class CrashesFragment: BaseViewModelFragment + crash?.let { + disabledAppsRepository.disabledForFlow(it.packageName) + } ?: flowOf(null) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + val wrapCrashLogLines get() = appPreferences.wrapCrashLogLines + val useSeparateNotificationsChannelsForCrashes get() = appPreferences.useSeparateNotificationsChannelsForCrashes + fun exportCrashToZip(uri: Uri) = launchCatching(ioDispatcher) { val (appCrash, crashLog) = crash.value ?: return@launchCatching @@ -71,6 +92,10 @@ class CrashDetailsViewModel @Inject constructor( } } + fun changeBlacklist(appCrash: AppCrash) = launchCatching { + disabledAppsRepository.checkApp(appCrash.packageName) + } + fun deleteCrash(appCrash: AppCrash) = launchCatching { crashesRepository.delete(appCrash) } diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt similarity index 100% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt similarity index 64% rename from feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt rename to feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt index 6062709c..6f3860c5 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt +++ b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt @@ -1,34 +1,42 @@ package com.f0x1d.logfox.feature.crashes.viewmodel.list import android.app.Application +import android.content.Context import androidx.lifecycle.viewModelScope import com.f0x1d.logfox.arch.di.DefaultDispatcher import com.f0x1d.logfox.arch.viewmodel.BaseViewModel import com.f0x1d.logfox.database.entity.AppCrash import com.f0x1d.logfox.database.entity.AppCrashesCount +import com.f0x1d.logfox.database.entity.DisabledApp +import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerResultHandler import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository +import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository +import com.f0x1d.logfox.model.InstalledApp import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort +import com.f0x1d.logfox.strings.Strings import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CrashesViewModel @Inject constructor( private val crashesRepository: CrashesRepository, + private val disabledAppsRepository: DisabledAppsRepository, private val appPreferences: AppPreferences, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, application: Application, -): BaseViewModel(application) { +): BaseViewModel(application), AppsPickerResultHandler { val currentSort get() = appPreferences.crashesSortType.get() val currentSortInReversedOrder get() = appPreferences.crashesSortReversedOrder.get() @@ -73,22 +81,20 @@ class CrashesViewModel @Inject constructor( val searchedCrashes = combine( crashesRepository.getAllAsFlow(), query, - ) { crashes, query -> - crashes to query - }.debounce( - timeoutMillis = 100, - ).map { (crashes, query) -> - crashes.filter { crash -> - crash.packageName.contains(query, ignoreCase = true) - || crash.appName?.contains(query, ignoreCase = true) == true - }.map { AppCrashesCount(it) } - }.flowOn( - defaultDispatcher, - ).stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = emptyList(), - ) + ) { crashes, query -> crashes to query } + .map { (crashes, query) -> + crashes.filter { crash -> + crash.packageName.contains(query, ignoreCase = true) + || crash.appName?.contains(query, ignoreCase = true) == true + }.map { AppCrashesCount(it) } + } + .distinctUntilChanged() + .flowOn(defaultDispatcher) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList(), + ) fun updateQuery(query: String) = this.query.update { query } @@ -109,9 +115,36 @@ class CrashesViewModel @Inject constructor( crashesRepository.clear() } + override val supportsMultiplySelection: Boolean = true + + override val checkedAppPackageNames: Flow> = + disabledAppsRepository.getAllAsFlow().map { apps -> + apps.map(DisabledApp::packageName).toSet() + } + + override fun providePickerTopAppBarTitle(context: Context): String = + context.getString(Strings.blacklist) + + override fun onAppChecked(app: InstalledApp, checked: Boolean) { + viewModelScope.launch { + disabledAppsRepository.checkApp(app.packageName, checked) + } + } + + override fun onAppSelected(app: InstalledApp): Boolean { + viewModelScope.launch { + disabledAppsRepository.checkApp(app.packageName) + } + return false + } + private data class CrashesWithSort( val crashes: List, val sortType: CrashesSort, val sortInReversedOrder: Boolean, ) + + companion object { + private const val SEARCH_DEBOUNCE_MILLIS = 500L + } } diff --git a/feature/feature-crashes/src/main/res/layout/dialog_sorting.xml b/feature/crashes/src/main/res/layout/dialog_sorting.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/dialog_sorting.xml rename to feature/crashes/src/main/res/layout/dialog_sorting.xml diff --git a/feature/feature-crashes/src/main/res/layout/fragment_app_crashes.xml b/feature/crashes/src/main/res/layout/fragment_app_crashes.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/fragment_app_crashes.xml rename to feature/crashes/src/main/res/layout/fragment_app_crashes.xml diff --git a/feature/feature-crashes/src/main/res/layout/fragment_crash_details.xml b/feature/crashes/src/main/res/layout/fragment_crash_details.xml similarity index 90% rename from feature/feature-crashes/src/main/res/layout/fragment_crash_details.xml rename to feature/crashes/src/main/res/layout/fragment_crash_details.xml index 297cd88c..a8c77581 100644 --- a/feature/feature-crashes/src/main/res/layout/fragment_crash_details.xml +++ b/feature/crashes/src/main/res/layout/fragment_crash_details.xml @@ -140,22 +140,29 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/actions_card"> - + + + android:layout_height="match_parent" + android:scrollbars="none"> - + diff --git a/feature/feature-crashes/src/main/res/layout/fragment_crashes.xml b/feature/crashes/src/main/res/layout/fragment_crashes.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/fragment_crashes.xml rename to feature/crashes/src/main/res/layout/fragment_crashes.xml diff --git a/feature/feature-crashes/src/main/res/layout/item_crash.xml b/feature/crashes/src/main/res/layout/item_crash.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/item_crash.xml rename to feature/crashes/src/main/res/layout/item_crash.xml diff --git a/feature/feature-crashes/src/main/res/layout/item_sort.xml b/feature/crashes/src/main/res/layout/item_sort.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/item_sort.xml rename to feature/crashes/src/main/res/layout/item_sort.xml diff --git a/feature/feature-crashes/src/main/res/layout/placeholder_crashes.xml b/feature/crashes/src/main/res/layout/placeholder_crashes.xml similarity index 100% rename from feature/feature-crashes/src/main/res/layout/placeholder_crashes.xml rename to feature/crashes/src/main/res/layout/placeholder_crashes.xml diff --git a/feature/feature-crashes/src/main/res/menu/crash_details_menu.xml b/feature/crashes/src/main/res/menu/crash_details_menu.xml similarity index 61% rename from feature/feature-crashes/src/main/res/menu/crash_details_menu.xml rename to feature/crashes/src/main/res/menu/crash_details_menu.xml index 4114d9a9..39819cdd 100644 --- a/feature/feature-crashes/src/main/res/menu/crash_details_menu.xml +++ b/feature/crashes/src/main/res/menu/crash_details_menu.xml @@ -2,6 +2,13 @@ + + + + + + () { - - override val viewModel by viewModels() - - private val zipCrashLauncher = registerForActivityResult( - ActivityResultContracts.CreateDocument("application/zip"), - ) { - viewModel.exportCrashToZip(it ?: return@registerForActivityResult) - } - - override fun inflateBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ) = FragmentCrashDetailsBinding.inflate(inflater, container, false) - - override fun FragmentCrashDetailsBinding.onViewCreated(view: View, savedInstanceState: Bundle?) { - scrollView.applyInsetter { - type(navigationBars = true) { - padding(vertical = true) - } - } - - toolbar.setupBackButtonForNavController() - - viewModel.crash.collectWithLifecycle { - setupFor(it ?: return@collectWithLifecycle) - } - } - - @SuppressLint("InlinedApi") - private fun FragmentCrashDetailsBinding.setupFor(item: Pair) { - val (appCrash, crashLog) = item - - toolbar.menu.apply { - setClickListenerOn(R.id.info_item) { - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", appCrash.packageName, null) - }.let(::startActivity) - } - - findItem(R.id.notifications_item).setVisible( - notificationsChannelsAvailable - && viewModel.appPreferences.useSeparateNotificationsChannelsForCrashes - ) - setClickListenerOn(R.id.notifications_item) { - Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - putExtra(Settings.EXTRA_CHANNEL_ID, appCrash.notificationChannelId) - }.let(::startActivity) - } - setClickListenerOn(R.id.delete_item) { - showAreYouSureDeleteDialog { - viewModel.deleteCrash(appCrash) - findNavController().popBackStack() - } - } - } - - appLogo.loadIcon(appCrash.packageName) - appName.text = appCrash.appName ?: getString(Strings.unknown) - appPackage.text = appCrash.packageName - - copyButton.setOnClickListener { - requireContext().copyText(crashLog ?: "") - snackbar(Strings.text_copied) - } - - shareButton.setOnClickListener { - requireContext().shareIntent(crashLog ?: "") - } - - zipButton.setOnClickListener { - val pkg = appCrash.packageName.replace(".", "-") - val formattedDate = viewModel.dateTimeFormatter.formatForExport(appCrash.dateAndTime) - - zipCrashLauncher.launch("crash-$pkg-$formattedDate.zip") - } - - logText.text = crashLog - } -} diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/AppsAdapter.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/AppsAdapter.kt deleted file mode 100644 index 6f7988e4..00000000 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/AppsAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.f0x1d.logfox.feature.filters.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.f0x1d.logfox.arch.adapter.BaseListAdapter -import com.f0x1d.logfox.feature.filters.databinding.ItemAppBinding -import com.f0x1d.logfox.feature.filters.ui.viewholder.AppViewHolder -import com.f0x1d.logfox.model.InstalledApp -import com.f0x1d.logfox.model.diffCallback - -class AppsAdapter( - private val click: (InstalledApp) -> Unit -): BaseListAdapter(diffCallback()) { - - override fun createHolder(layoutInflater: LayoutInflater, parent: ViewGroup) = AppViewHolder( - binding = ItemAppBinding.inflate(layoutInflater, parent, false), - click = click, - ) -} diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt deleted file mode 100644 index 19340ec6..00000000 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.f0x1d.logfox.feature.filters.ui.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.viewModels -import androidx.hilt.navigation.fragment.hiltNavGraphViewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment -import com.f0x1d.logfox.feature.filters.R -import com.f0x1d.logfox.feature.filters.adapter.AppsAdapter -import com.f0x1d.logfox.feature.filters.databinding.FragmentChooseAppBinding -import com.f0x1d.logfox.feature.filters.viewmodel.ChooseAppViewModel -import com.f0x1d.logfox.feature.filters.viewmodel.EditFilterViewModel -import com.f0x1d.logfox.model.InstalledApp -import com.f0x1d.logfox.navigation.Directions -import com.f0x1d.logfox.ui.density.dpToPx -import com.f0x1d.logfox.ui.view.setClickListenerOn -import com.f0x1d.logfox.ui.view.setupBackButtonForNavController -import com.google.android.material.divider.MaterialDividerItemDecoration -import com.google.android.material.search.SearchView -import dagger.hilt.android.AndroidEntryPoint -import dev.chrisbanes.insetter.applyInsetter - -@AndroidEntryPoint -class ChooseAppFragment: BaseViewModelFragment() { - - override val viewModel by viewModels() - - private val editFilterViewModel by hiltNavGraphViewModels(Directions.editFilterFragment) - - private val onAppClicked: (InstalledApp) -> Unit = { - editFilterViewModel.selectApp(it) - findNavController().popBackStack() - } - private val appsAdapter = AppsAdapter(onAppClicked) - private val searchedAppsAdapter = AppsAdapter(onAppClicked) - - private val closeSearchOnBackPressedCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - binding.searchView.hide() - } - } - - override fun inflateBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ) = FragmentChooseAppBinding.inflate(inflater, container, false) - - override fun FragmentChooseAppBinding.onViewCreated(view: View, savedInstanceState: Bundle?) { - appsRecycler.applyInsetter { - type(navigationBars = true) { - padding(vertical = true) - } - } - - searchBar.apply { - setupBackButtonForNavController() - - menu.setClickListenerOn(R.id.search_item) { - searchView.show() - } - } - - searchView.apply { - editText.doAfterTextChanged { text -> - viewModel.updateQuery(text?.toString().orEmpty()) - } - - addTransitionListener { _, _, newState -> - closeSearchOnBackPressedCallback.isEnabled = newState == SearchView.TransitionState.SHOWN - } - } - - listOf(appsRecycler, searchedAppsRecycler).forEach { - it.apply { - layoutManager = LinearLayoutManager(requireContext()) - - addItemDecoration( - MaterialDividerItemDecoration( - requireContext(), - LinearLayoutManager.VERTICAL, - ).apply { - dividerInsetStart = 80.dpToPx.toInt() - dividerInsetEnd = 10.dpToPx.toInt() - isLastItemDecorated = false - } - ) - } - } - - appsRecycler.adapter = appsAdapter - searchedAppsRecycler.adapter = searchedAppsAdapter - - viewModel.apps.collectWithLifecycle { - appsAdapter.submitList(it) - } - viewModel.searchedApps.collectWithLifecycle { - searchedAppsAdapter.submitList(it) - } - - requireActivity().onBackPressedDispatcher.apply { - addCallback(viewLifecycleOwner, closeSearchOnBackPressedCallback) - } - } -} diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/AppViewHolder.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/AppViewHolder.kt deleted file mode 100644 index f1180206..00000000 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/AppViewHolder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.f0x1d.logfox.feature.filters.ui.viewholder - -import com.bumptech.glide.Glide -import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder -import com.f0x1d.logfox.feature.filters.databinding.ItemAppBinding -import com.f0x1d.logfox.model.InstalledApp -import com.f0x1d.logfox.ui.view.loadIcon - -class AppViewHolder( - binding: ItemAppBinding, - click: (InstalledApp) -> Unit -): BaseViewHolder(binding) { - - init { - binding.apply { - root.setOnClickListener { - click(currentItem ?: return@setOnClickListener) - } - } - } - - override fun ItemAppBinding.bindTo(data: InstalledApp) { - icon.loadIcon(data.packageName) - - title.text = data.title - packageNameText.text = data.packageName - } - - override fun ItemAppBinding.recycle() = Glide.with(icon).clear(icon) -} diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt deleted file mode 100644 index 8799af2e..00000000 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.f0x1d.logfox.feature.filters.viewmodel - -import android.app.Application -import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.arch.di.DefaultDispatcher -import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.model.InstalledApp -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -@HiltViewModel -class ChooseAppViewModel @Inject constructor( - @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, - application: Application, -): BaseViewModel(application) { - - val apps = MutableStateFlow(emptyList()) - val query = MutableStateFlow("") - - val searchedApps = combine(apps, query) { apps, query -> - apps to query - }.map { - it.first.filter { app -> - app.title.toString().contains(it.second) || app.packageName.contains(it.second) - } - }.flowOn( - defaultDispatcher, - ).stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = emptyList(), - ) - - init { - load() - } - - fun updateQuery(text: String) = query.update { text } - - private fun load() = launchCatching(defaultDispatcher) { - val packageManager = ctx.packageManager - - val installedApps = packageManager.getInstalledPackages(0).map { - InstalledApp( - title = it.applicationInfo.loadLabel(packageManager), - packageName = it.packageName, - ) - }.sortedBy { - it.title.toString() - } - - apps.update { - installedApps - } - } -} diff --git a/feature/feature-filters/src/main/res/layout/fragment_choose_app.xml b/feature/feature-filters/src/main/res/layout/fragment_choose_app.xml deleted file mode 100644 index 446de45c..00000000 --- a/feature/feature-filters/src/main/res/layout/fragment_choose_app.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/feature/feature-filters/src/main/res/layout/item_app.xml b/feature/feature-filters/src/main/res/layout/item_app.xml deleted file mode 100644 index 00db54dd..00000000 --- a/feature/feature-filters/src/main/res/layout/item_app.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/feature/feature-filters/src/main/res/menu/choose_app_menu.xml b/feature/feature-filters/src/main/res/menu/choose_app_menu.xml deleted file mode 100644 index e643fa06..00000000 --- a/feature/feature-filters/src/main/res/menu/choose_app_menu.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/feature-logging/build.gradle.kts b/feature/feature-logging/build.gradle.kts deleted file mode 100644 index e89d798c..00000000 --- a/feature/feature-logging/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id("logfox.android.feature") -} - -android.namespace = "com.f0x1d.logfox.feature.logging" - -dependencies { - implementation(project(":feature:feature-crashes-core")) - implementation(project(":feature:feature-filters-core")) - implementation(project(":feature:feature-logging-core")) - implementation(project(":feature:feature-recordings-core")) -} diff --git a/feature/feature-recordings/.gitignore b/feature/feature-recordings/.gitignore deleted file mode 100644 index 567609b1..00000000 --- a/feature/feature-recordings/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/feature/feature-settings/.gitignore b/feature/feature-settings/.gitignore deleted file mode 100644 index 567609b1..00000000 --- a/feature/feature-settings/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/feature/feature-setup/.gitignore b/feature/feature-setup/.gitignore deleted file mode 100644 index 567609b1..00000000 --- a/feature/feature-setup/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/feature/feature-setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png b/feature/feature-setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png deleted file mode 100644 index e220c4a140cd8acb76d3bb386084310722ddecbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39741 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
    @Oc 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 UserFilter) = update(newValue()) override fun getAllAsFlow(): Flow> = - database.userFilters().getAllAsFlow().flowOn(ioDispatcher) + database.userFilters().getAllAsFlow() + .distinctUntilChanged() + .flowOn(ioDispatcher) override fun getByIdAsFlow(id: Long): Flow = database.userFilters().getByIdAsFlow(id).flowOn(ioDispatcher) diff --git a/core/core-ui-compose/.gitignore b/feature/filters/.gitignore similarity index 100% rename from core/core-ui-compose/.gitignore rename to feature/filters/.gitignore diff --git a/feature/feature-filters/build.gradle.kts b/feature/filters/build.gradle.kts similarity index 55% rename from feature/feature-filters/build.gradle.kts rename to feature/filters/build.gradle.kts index c807dbd5..045b15ad 100644 --- a/feature/feature-filters/build.gradle.kts +++ b/feature/filters/build.gradle.kts @@ -5,10 +5,8 @@ plugins { android.namespace = "com.f0x1d.logfox.feature.filters" dependencies { - implementation(project(":feature:feature-filters-core")) + implementation(projects.feature.appsPicker) + implementation(projects.feature.filtersCore) implementation(libs.gson) - - implementation(libs.glide) - ksp(libs.glide.compiler) } diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt similarity index 100% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt similarity index 100% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt similarity index 93% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt index 5be68334..3fa577c6 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt +++ b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt @@ -9,13 +9,13 @@ import android.widget.EditText import androidx.activity.result.contract.ActivityResultContracts import androidx.core.widget.doAfterTextChanged import androidx.hilt.navigation.fragment.hiltNavGraphViewModels -import androidx.lifecycle.asLiveData import androidx.navigation.fragment.findNavController import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment +import com.f0x1d.logfox.arch.viewmodel.Event import com.f0x1d.logfox.feature.filters.R import com.f0x1d.logfox.feature.filters.databinding.FragmentEditFilterBinding import com.f0x1d.logfox.feature.filters.viewmodel.EditFilterViewModel -import com.f0x1d.logfox.model.event.Event +import com.f0x1d.logfox.feature.filters.viewmodel.UpdatePackageNameText import com.f0x1d.logfox.model.logline.LogLevel import com.f0x1d.logfox.navigation.Directions import com.f0x1d.logfox.strings.Strings @@ -78,7 +78,7 @@ class EditFilterFragment: BaseViewModelFragment - updateIncludingButton(enabled) - } + viewModel.including.collectWithLifecycle { enabled -> + updateIncludingButton(enabled) + } + viewModel.filter.collectWithLifecycle { viewModel.uid.toText(uidText) viewModel.pid.toText(pidText) viewModel.tid.toText(tidText) @@ -117,8 +117,10 @@ class EditFilterFragment: BaseViewModelFragment { + super.onEvent(event) + + when (event) { + is UpdatePackageNameText -> { binding.packageNameText.setText(viewModel.packageName.value) } } diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt similarity index 100% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt similarity index 100% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt similarity index 89% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt index 9246ebb6..726de3cd 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt +++ b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt @@ -5,7 +5,9 @@ import android.net.Uri import androidx.lifecycle.viewModelScope import com.f0x1d.logfox.arch.di.IODispatcher import com.f0x1d.logfox.arch.viewmodel.BaseViewModel +import com.f0x1d.logfox.arch.viewmodel.Event import com.f0x1d.logfox.database.entity.UserFilter +import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerResultHandler import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository import com.f0x1d.logfox.feature.filters.di.FilterId import com.f0x1d.logfox.model.InstalledApp @@ -29,11 +31,7 @@ class EditFilterViewModel @Inject constructor( private val gson: Gson, @IODispatcher private val ioDispatcher: CoroutineDispatcher, application: Application, -): BaseViewModel(application) { - - companion object { - const val EVENT_TYPE_UPDATE_PACKAGE_NAME_TEXT = "update_package_name_text" - } +): BaseViewModel(application), AppsPickerResultHandler { val filter = filtersRepository.getByIdAsFlow(filterId ?: -1L) .distinctUntilChanged() @@ -99,10 +97,13 @@ class EditFilterViewModel @Inject constructor( enabledLogLevels[which] = filtering } - fun selectApp(app: InstalledApp) = packageName.update { - app.packageName - }.also { - sendEvent(EVENT_TYPE_UPDATE_PACKAGE_NAME_TEXT) + override fun onAppSelected(app: InstalledApp): Boolean { + packageName.update { + app.packageName + }.also { + sendEvent(UpdatePackageNameText) + } + return true } private fun List.toEnabledLogLevels() = mapIndexed { index, value -> @@ -112,3 +113,5 @@ class EditFilterViewModel @Inject constructor( null }.filterNotNull() } + +data object UpdatePackageNameText : Event diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt similarity index 100% rename from feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt rename to feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt diff --git a/feature/feature-filters/src/main/res/layout/fragment_edit_filter.xml b/feature/filters/src/main/res/layout/fragment_edit_filter.xml similarity index 100% rename from feature/feature-filters/src/main/res/layout/fragment_edit_filter.xml rename to feature/filters/src/main/res/layout/fragment_edit_filter.xml diff --git a/feature/feature-filters/src/main/res/layout/fragment_filters.xml b/feature/filters/src/main/res/layout/fragment_filters.xml similarity index 100% rename from feature/feature-filters/src/main/res/layout/fragment_filters.xml rename to feature/filters/src/main/res/layout/fragment_filters.xml diff --git a/feature/feature-filters/src/main/res/layout/item_filter.xml b/feature/filters/src/main/res/layout/item_filter.xml similarity index 100% rename from feature/feature-filters/src/main/res/layout/item_filter.xml rename to feature/filters/src/main/res/layout/item_filter.xml diff --git a/feature/feature-filters/src/main/res/layout/placeholder_filters.xml b/feature/filters/src/main/res/layout/placeholder_filters.xml similarity index 100% rename from feature/feature-filters/src/main/res/layout/placeholder_filters.xml rename to feature/filters/src/main/res/layout/placeholder_filters.xml diff --git a/feature/feature-filters/src/main/res/menu/edit_filter_menu.xml b/feature/filters/src/main/res/menu/edit_filter_menu.xml similarity index 100% rename from feature/feature-filters/src/main/res/menu/edit_filter_menu.xml rename to feature/filters/src/main/res/menu/edit_filter_menu.xml diff --git a/feature/feature-filters/src/main/res/menu/filters_menu.xml b/feature/filters/src/main/res/menu/filters_menu.xml similarity index 100% rename from feature/feature-filters/src/main/res/menu/filters_menu.xml rename to feature/filters/src/main/res/menu/filters_menu.xml diff --git a/feature/feature-crashes-core/.gitignore b/feature/logging-core/.gitignore similarity index 100% rename from feature/feature-crashes-core/.gitignore rename to feature/logging-core/.gitignore diff --git a/feature/feature-logging-core/build.gradle.kts b/feature/logging-core/build.gradle.kts similarity index 100% rename from feature/feature-logging-core/build.gradle.kts rename to feature/logging-core/build.gradle.kts diff --git a/feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt similarity index 100% rename from feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt rename to feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt diff --git a/feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt similarity index 100% rename from feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt rename to feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt diff --git a/feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt similarity index 100% rename from feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt rename to feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt diff --git a/feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt similarity index 100% rename from feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt rename to feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt diff --git a/feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt similarity index 100% rename from feature/feature-logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt rename to feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt diff --git a/feature/logging/.gitignore b/feature/logging/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/feature/logging/.gitignore @@ -0,0 +1 @@ +/build diff --git a/feature/logging/build.gradle.kts b/feature/logging/build.gradle.kts new file mode 100644 index 00000000..26490df9 --- /dev/null +++ b/feature/logging/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("logfox.android.feature") +} + +android.namespace = "com.f0x1d.logfox.feature.logging" + +dependencies { + implementation(projects.feature.crashesCore) + implementation(projects.feature.filtersCore) + implementation(projects.feature.loggingCore) + implementation(projects.feature.recordingsCore) +} diff --git a/feature/feature-logging/src/main/AndroidManifest.xml b/feature/logging/src/main/AndroidManifest.xml similarity index 100% rename from feature/feature-logging/src/main/AndroidManifest.xml rename to feature/logging/src/main/AndroidManifest.xml diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt similarity index 74% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt index 2354819d..4c4a68d1 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt +++ b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt @@ -7,10 +7,12 @@ import com.f0x1d.logfox.arch.adapter.BaseListAdapter import com.f0x1d.logfox.feature.logging.databinding.ItemLogBinding import com.f0x1d.logfox.model.diffCallback import com.f0x1d.logfox.model.logline.LogLine -import com.f0x1d.logfox.preferences.shared.AppPreferences +import com.f0x1d.logfox.model.preferences.ShowLogValues class LogsAdapter( - private val appPreferences: AppPreferences, + private val textSizeProvider: () -> Float, + private val logsExpandedProvider: () -> Boolean, + private val logsFormatProvider: () -> ShowLogValues, private val selectedItem: (LogLine, Boolean) -> Unit, private val copyLog: (LogLine) -> Unit ): BaseListAdapter(diffCallback()) { @@ -22,9 +24,9 @@ class LogsAdapter( notifyItemRangeChanged(0, itemCount) } - val textSize get() = appPreferences.logsTextSize.toFloat() - val logsExpanded get() = appPreferences.logsExpanded - val logsFormat get() = appPreferences.showLogValues + val textSize get() = textSizeProvider() + val logsExpanded get() = logsExpandedProvider() + val logsFormat get() = logsFormatProvider() override fun createHolder(layoutInflater: LayoutInflater, parent: ViewGroup) = LogViewHolder( binding = ItemLogBinding.inflate(layoutInflater, parent, false), diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt similarity index 96% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt index 37f3a461..fb0ef88f 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt +++ b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt @@ -7,10 +7,13 @@ import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope +import com.f0x1d.logfox.arch.EXIT_APP_INTENT_ID +import com.f0x1d.logfox.arch.LOGGING_STATUS_CHANNEL_ID +import com.f0x1d.logfox.arch.OPEN_APP_INTENT_ID +import com.f0x1d.logfox.arch.activityManager import com.f0x1d.logfox.arch.di.DefaultDispatcher -import com.f0x1d.logfox.context.LOGGING_STATUS_CHANNEL_ID -import com.f0x1d.logfox.context.activityManager -import com.f0x1d.logfox.context.toast +import com.f0x1d.logfox.arch.makeServicePendingIntent +import com.f0x1d.logfox.arch.toast import com.f0x1d.logfox.database.entity.UserFilter import com.f0x1d.logfox.feature.crashes.core.controller.CrashesController import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository @@ -18,9 +21,6 @@ import com.f0x1d.logfox.feature.logging.core.model.suits import com.f0x1d.logfox.feature.logging.core.repository.LoggingRepository import com.f0x1d.logfox.feature.logging.core.store.LoggingStore import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController -import com.f0x1d.logfox.intents.EXIT_APP_INTENT_ID -import com.f0x1d.logfox.intents.OPEN_APP_INTENT_ID -import com.f0x1d.logfox.intents.makeServicePendingIntent import com.f0x1d.logfox.model.exception.TerminalNotSupportedException import com.f0x1d.logfox.model.logline.LogLine import com.f0x1d.logfox.preferences.shared.AppPreferences diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt similarity index 94% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt index c38a9868..b49f0570 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt +++ b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt @@ -14,10 +14,10 @@ import androidx.recyclerview.widget.RecyclerView import com.f0x1d.feature.logging.adapter.LogsAdapter import com.f0x1d.feature.logging.service.LoggingService import com.f0x1d.feature.logging.viewmodel.LogsViewModel +import com.f0x1d.logfox.arch.copyText +import com.f0x1d.logfox.arch.isHorizontalOrientation +import com.f0x1d.logfox.arch.sendService import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment -import com.f0x1d.logfox.context.copyText -import com.f0x1d.logfox.context.isHorizontalOrientation -import com.f0x1d.logfox.context.sendService import com.f0x1d.logfox.feature.logging.R import com.f0x1d.logfox.feature.logging.databinding.FragmentLogsBinding import com.f0x1d.logfox.model.logline.LogLine @@ -40,15 +40,13 @@ class LogsFragment: BaseViewModelFragment() private val adapter by lazy { LogsAdapter( - appPreferences = viewModel.appPreferences, + textSizeProvider = viewModel::logsTextSize, + logsExpandedProvider = viewModel::logsExpanded, + logsFormatProvider = viewModel::logsFormat, selectedItem = viewModel::selectLine, copyLog = { requireContext().copyText( - viewModel.appPreferences.originalOf( - logLine = it, - formatDate = viewModel.dateTimeFormatter::formatDate, - formatTime = viewModel.dateTimeFormatter::formatTime, - ) + text = viewModel.originalOf(it), ) snackbar(Strings.text_copied) }, @@ -120,7 +118,7 @@ class LogsFragment: BaseViewModelFragment() } setClickListenerOn(R.id.export_selected_item) { exportLogsLauncher.launch( - "${viewModel.dateTimeFormatter.formatForExport(System.currentTimeMillis())}.log" + "${viewModel.formatForExport(System.currentTimeMillis())}.log" ) } setClickListenerOn(R.id.clear_item) { diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt similarity index 88% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt index edae13d2..ee39a5ee 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt +++ b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt @@ -36,12 +36,12 @@ class LogsViewModel @Inject constructor( private val loggingStore: LoggingStore, private val filtersRepository: FiltersRepository, private val recordingsRepository: RecordingsRepository, - val appPreferences: AppPreferences, - val dateTimeFormatter: DateTimeFormatter, + private val appPreferences: AppPreferences, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, @IODispatcher private val ioDispatcher: CoroutineDispatcher, + dateTimeFormatter: DateTimeFormatter, application: Application, -): BaseViewModel(application) { +): BaseViewModel(application), DateTimeFormatter by dateTimeFormatter { val query = MutableStateFlow(null) val queryAndFilters = query.combine( @@ -58,8 +58,8 @@ class LogsViewModel @Inject constructor( val selectedItemsContent get() = selectedItems.value.joinToString("\n") { line -> appPreferences.originalOf( logLine = line, - formatDate = dateTimeFormatter::formatDate, - formatTime = dateTimeFormatter::formatTime, + formatDate = ::formatDate, + formatTime = ::formatTime, ) } @@ -106,6 +106,9 @@ class LogsViewModel @Inject constructor( ) val resumeLoggingWithBottomTouch get() = appPreferences.resumeLoggingWithBottomTouch + val logsTextSize get() = appPreferences.logsTextSize.toFloat() + val logsExpanded get() = appPreferences.logsExpanded + val logsFormat get() = appPreferences.showLogValues fun selectLine(logLine: LogLine, selected: Boolean) = selectedItems.updateSet { if (selected) add( @@ -137,8 +140,8 @@ class LogsViewModel @Inject constructor( selectedItems.value.joinToString("\n") { line -> appPreferences.originalOf( logLine = line, - formatDate = dateTimeFormatter::formatDate, - formatTime = dateTimeFormatter::formatTime, + formatDate = ::formatDate, + formatTime = ::formatTime, ) }.encodeToByteArray() ) @@ -155,6 +158,12 @@ class LogsViewModel @Inject constructor( fun pause() = paused.update { true } fun resume() = paused.update { false } + fun originalOf(logLine: LogLine): String = appPreferences.originalOf( + logLine = logLine, + formatDate = ::formatDate, + formatTime = ::formatTime, + ) + private fun MutableStateFlow>.updateSet(block: MutableSet.() -> Unit) = update { it.toMutableSet().apply(block).toSet() } diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt similarity index 100% rename from feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt rename to feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt diff --git a/feature/feature-logging/src/main/res/layout/fragment_logs.xml b/feature/logging/src/main/res/layout/fragment_logs.xml similarity index 100% rename from feature/feature-logging/src/main/res/layout/fragment_logs.xml rename to feature/logging/src/main/res/layout/fragment_logs.xml diff --git a/feature/feature-logging/src/main/res/layout/fragment_logs_extended_copy.xml b/feature/logging/src/main/res/layout/fragment_logs_extended_copy.xml similarity index 100% rename from feature/feature-logging/src/main/res/layout/fragment_logs_extended_copy.xml rename to feature/logging/src/main/res/layout/fragment_logs_extended_copy.xml diff --git a/feature/feature-logging/src/main/res/layout/item_log.xml b/feature/logging/src/main/res/layout/item_log.xml similarity index 100% rename from feature/feature-logging/src/main/res/layout/item_log.xml rename to feature/logging/src/main/res/layout/item_log.xml diff --git a/feature/feature-logging/src/main/res/layout/placeholder_logs.xml b/feature/logging/src/main/res/layout/placeholder_logs.xml similarity index 100% rename from feature/feature-logging/src/main/res/layout/placeholder_logs.xml rename to feature/logging/src/main/res/layout/placeholder_logs.xml diff --git a/feature/feature-logging/src/main/res/layout/sheet_search.xml b/feature/logging/src/main/res/layout/sheet_search.xml similarity index 100% rename from feature/feature-logging/src/main/res/layout/sheet_search.xml rename to feature/logging/src/main/res/layout/sheet_search.xml diff --git a/feature/feature-logging/src/main/res/menu/log_menu.xml b/feature/logging/src/main/res/menu/log_menu.xml similarity index 100% rename from feature/feature-logging/src/main/res/menu/log_menu.xml rename to feature/logging/src/main/res/menu/log_menu.xml diff --git a/feature/feature-logging/src/main/res/menu/logs_menu.xml b/feature/logging/src/main/res/menu/logs_menu.xml similarity index 100% rename from feature/feature-logging/src/main/res/menu/logs_menu.xml rename to feature/logging/src/main/res/menu/logs_menu.xml diff --git a/feature/feature-filters-core/.gitignore b/feature/recordings-core/.gitignore similarity index 100% rename from feature/feature-filters-core/.gitignore rename to feature/recordings-core/.gitignore diff --git a/feature/feature-recordings-core/build.gradle.kts b/feature/recordings-core/build.gradle.kts similarity index 67% rename from feature/feature-recordings-core/build.gradle.kts rename to feature/recordings-core/build.gradle.kts index 39630fa2..3812f347 100644 --- a/feature/feature-recordings-core/build.gradle.kts +++ b/feature/recordings-core/build.gradle.kts @@ -5,5 +5,5 @@ plugins { android.namespace = "com.f0x1d.logfox.feature.recordings.core" dependencies { - implementation(project(":feature:feature-logging-core")) + implementation(projects.feature.loggingCore) } diff --git a/feature/feature-recordings-core/src/main/AndroidManifest.xml b/feature/recordings-core/src/main/AndroidManifest.xml similarity index 100% rename from feature/feature-recordings-core/src/main/AndroidManifest.xml rename to feature/recordings-core/src/main/AndroidManifest.xml diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt similarity index 100% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt similarity index 90% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt index 43386840..349a2a2a 100644 --- a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt +++ b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt @@ -3,14 +3,14 @@ package com.f0x1d.logfox.feature.recordings.core.controller import android.annotation.SuppressLint import android.content.Context import androidx.core.app.NotificationCompat -import com.f0x1d.logfox.context.RECORDING_STATUS_CHANNEL_ID -import com.f0x1d.logfox.context.doIfNotificationsAllowed -import com.f0x1d.logfox.context.notificationManagerCompat +import com.f0x1d.logfox.arch.PAUSE_RECORDING_INTENT_ID +import com.f0x1d.logfox.arch.RECORDING_STATUS_CHANNEL_ID +import com.f0x1d.logfox.arch.RESUME_RECORDING_INTENT_ID +import com.f0x1d.logfox.arch.STOP_RECORDING_INTENT_ID +import com.f0x1d.logfox.arch.doIfNotificationsAllowed +import com.f0x1d.logfox.arch.makeBroadcastPendingIntent +import com.f0x1d.logfox.arch.notificationManagerCompat import com.f0x1d.logfox.feature.recordings.core.receiver.RecordingReceiver -import com.f0x1d.logfox.intents.PAUSE_RECORDING_INTENT_ID -import com.f0x1d.logfox.intents.RESUME_RECORDING_INTENT_ID -import com.f0x1d.logfox.intents.STOP_RECORDING_INTENT_ID -import com.f0x1d.logfox.intents.makeBroadcastPendingIntent import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.Icons import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt similarity index 100% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt similarity index 100% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt similarity index 100% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt similarity index 100% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt diff --git a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt similarity index 96% rename from feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt rename to feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt index 3c306076..9f1a24b3 100644 --- a/feature/feature-recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt +++ b/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt @@ -4,7 +4,7 @@ import android.content.Context import com.f0x1d.logfox.arch.di.IODispatcher import com.f0x1d.logfox.arch.di.MainDispatcher import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository -import com.f0x1d.logfox.context.toast +import com.f0x1d.logfox.arch.toast import com.f0x1d.logfox.database.AppDatabase import com.f0x1d.logfox.database.entity.LogRecording import com.f0x1d.logfox.datetime.DateTimeFormatter @@ -16,6 +16,7 @@ import com.f0x1d.logfox.terminals.base.Terminal import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import java.io.File @@ -113,7 +114,9 @@ internal class RecordingsRepositoryImpl @Inject constructor( ) override fun getAllAsFlow(): Flow> = - database.logRecordings().getAllAsFlow().flowOn(ioDispatcher) + database.logRecordings().getAllAsFlow() + .distinctUntilChanged() + .flowOn(ioDispatcher) override fun getByIdAsFlow(id: Long): Flow = database.logRecordings().getByIdAsFlow(id).flowOn(ioDispatcher) diff --git a/feature/feature-filters/.gitignore b/feature/recordings/.gitignore similarity index 100% rename from feature/feature-filters/.gitignore rename to feature/recordings/.gitignore diff --git a/feature/feature-recordings/build.gradle.kts b/feature/recordings/build.gradle.kts similarity index 65% rename from feature/feature-recordings/build.gradle.kts rename to feature/recordings/build.gradle.kts index 200c093f..307d674f 100644 --- a/feature/feature-recordings/build.gradle.kts +++ b/feature/recordings/build.gradle.kts @@ -5,5 +5,5 @@ plugins { android.namespace = "com.f0x1d.logfox.feature.recordings" dependencies { - implementation(project(":feature:feature-recordings-core")) + implementation(projects.feature.recordingsCore) } diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt similarity index 100% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt similarity index 100% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt similarity index 89% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt index fda16b00..6528933f 100644 --- a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt +++ b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt @@ -9,9 +9,9 @@ import androidx.core.os.bundleOf import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.f0x1d.logfox.arch.asUri +import com.f0x1d.logfox.arch.shareFileIntent import com.f0x1d.logfox.arch.ui.dialog.BaseViewModelBottomSheet -import com.f0x1d.logfox.context.asUri -import com.f0x1d.logfox.context.shareFileIntent import com.f0x1d.logfox.feature.recordings.databinding.SheetRecordingBinding import com.f0x1d.logfox.feature.recordings.viewmodel.RecordingViewModel import com.f0x1d.logfox.navigation.Directions @@ -57,13 +57,13 @@ class RecordingBottomSheet: BaseViewModelBottomSheet openDetails(event.consume()) + super.onEvent(event) + + when (event) { + is OpenRecording -> openDetails(event.recording) } } diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt similarity index 100% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt similarity index 93% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt index 17e9cdad..a7ad50c1 100644 --- a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt +++ b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt @@ -4,12 +4,12 @@ import android.app.Application import android.net.Uri import androidx.lifecycle.viewModelScope import com.f0x1d.logfox.arch.di.IODispatcher +import com.f0x1d.logfox.arch.io.exportToZip +import com.f0x1d.logfox.arch.io.putZipEntry import com.f0x1d.logfox.arch.viewmodel.BaseViewModel import com.f0x1d.logfox.datetime.DateTimeFormatter import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepository import com.f0x1d.logfox.feature.recordings.di.RecordingId -import com.f0x1d.logfox.io.exportToZip -import com.f0x1d.logfox.io.putZipEntry import com.f0x1d.logfox.model.deviceData import com.f0x1d.logfox.preferences.shared.AppPreferences import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,12 +27,12 @@ import javax.inject.Inject @HiltViewModel class RecordingViewModel @Inject constructor( @RecordingId val recordingId: Long, - val dateTimeFormatter: DateTimeFormatter, private val recordingsRepository: RecordingsRepository, private val appPreferences: AppPreferences, @IODispatcher private val ioDispatcher: CoroutineDispatcher, + dateTimeFormatter: DateTimeFormatter, application: Application -): BaseViewModel(application) { +): BaseViewModel(application), DateTimeFormatter by dateTimeFormatter { val recording = recordingsRepository.getByIdAsFlow(recordingId) .distinctUntilChanged() diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt similarity index 88% rename from feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt rename to feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt index 47c84fa1..1a40ded8 100644 --- a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt +++ b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt @@ -2,6 +2,7 @@ package com.f0x1d.logfox.feature.recordings.viewmodel import android.app.Application import com.f0x1d.logfox.arch.viewmodel.BaseViewModel +import com.f0x1d.logfox.arch.viewmodel.Event import com.f0x1d.logfox.database.entity.LogRecording import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController import com.f0x1d.logfox.feature.recordings.core.controller.RecordingState @@ -18,10 +19,6 @@ class RecordingsViewModel @Inject constructor( application: Application, ): BaseViewModel(application) { - companion object { - const val EVENT_TYPE_RECORDING_SAVED = "recording_saved" - } - val recordings = recordingsRepository.getAllAsFlow() .distinctUntilChanged() @@ -32,7 +29,7 @@ class RecordingsViewModel @Inject constructor( recordingController.record() else recordingController.end().also { - sendEvent(EVENT_TYPE_RECORDING_SAVED, it ?: return@also) + sendEvent(OpenRecording(it)) } } @@ -50,7 +47,7 @@ class RecordingsViewModel @Inject constructor( fun saveAll() = launchCatching { snackbar(Strings.saving_logs) recordingsRepository.saveAll().also { - sendEvent(EVENT_TYPE_RECORDING_SAVED, it) + sendEvent(OpenRecording(it)) } } @@ -58,3 +55,7 @@ class RecordingsViewModel @Inject constructor( recordingsRepository.delete(logRecording) } } + +data class OpenRecording( + val recording: LogRecording?, +) : Event diff --git a/feature/feature-recordings/src/main/res/layout/fragment_recordings.xml b/feature/recordings/src/main/res/layout/fragment_recordings.xml similarity index 100% rename from feature/feature-recordings/src/main/res/layout/fragment_recordings.xml rename to feature/recordings/src/main/res/layout/fragment_recordings.xml diff --git a/feature/feature-recordings/src/main/res/layout/item_recording.xml b/feature/recordings/src/main/res/layout/item_recording.xml similarity index 100% rename from feature/feature-recordings/src/main/res/layout/item_recording.xml rename to feature/recordings/src/main/res/layout/item_recording.xml diff --git a/feature/feature-recordings/src/main/res/layout/placeholder_recordings.xml b/feature/recordings/src/main/res/layout/placeholder_recordings.xml similarity index 100% rename from feature/feature-recordings/src/main/res/layout/placeholder_recordings.xml rename to feature/recordings/src/main/res/layout/placeholder_recordings.xml diff --git a/feature/feature-recordings/src/main/res/layout/sheet_recording.xml b/feature/recordings/src/main/res/layout/sheet_recording.xml similarity index 100% rename from feature/feature-recordings/src/main/res/layout/sheet_recording.xml rename to feature/recordings/src/main/res/layout/sheet_recording.xml diff --git a/feature/feature-recordings/src/main/res/menu/recordings_menu.xml b/feature/recordings/src/main/res/menu/recordings_menu.xml similarity index 100% rename from feature/feature-recordings/src/main/res/menu/recordings_menu.xml rename to feature/recordings/src/main/res/menu/recordings_menu.xml diff --git a/feature/feature-logging-core/.gitignore b/feature/settings/.gitignore similarity index 100% rename from feature/feature-logging-core/.gitignore rename to feature/settings/.gitignore diff --git a/feature/feature-settings/build.gradle.kts b/feature/settings/build.gradle.kts similarity index 100% rename from feature/feature-settings/build.gradle.kts rename to feature/settings/build.gradle.kts diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt similarity index 100% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt similarity index 100% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt similarity index 100% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt similarity index 100% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt similarity index 100% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt similarity index 95% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt index fa4113ac..b5f0858b 100644 --- a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt @@ -5,9 +5,9 @@ import android.content.Intent import android.os.Bundle import android.provider.Settings import androidx.preference.Preference +import com.f0x1d.logfox.arch.LOGGING_STATUS_CHANNEL_ID +import com.f0x1d.logfox.arch.hasNotificationsPermission import com.f0x1d.logfox.arch.notificationsChannelsAvailable -import com.f0x1d.logfox.context.LOGGING_STATUS_CHANNEL_ID -import com.f0x1d.logfox.context.hasNotificationsPermission import com.f0x1d.logfox.feature.settings.R import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.strings.Strings diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt similarity index 99% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt index 7bdf49ad..f1c76cd6 100644 --- a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.SwitchPreferenceCompat import com.f0x1d.logfox.arch.isAtLeastAndroid13 -import com.f0x1d.logfox.context.toast +import com.f0x1d.logfox.arch.toast import com.f0x1d.logfox.feature.settings.LoggingServiceDelegate import com.f0x1d.logfox.feature.settings.R import com.f0x1d.logfox.feature.settings.fillWithStrings diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt similarity index 95% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt index dd7811e5..045ec88a 100644 --- a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt @@ -4,7 +4,8 @@ import android.os.Bundle import android.text.InputType import androidx.appcompat.app.AppCompatDelegate import androidx.preference.Preference -import com.f0x1d.logfox.context.catchingNotNumber +import com.f0x1d.logfox.arch.catchingNotNumber +import com.f0x1d.logfox.arch.monetAvailable import com.f0x1d.logfox.feature.settings.R import com.f0x1d.logfox.feature.settings.fillWithStrings import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment @@ -56,6 +57,14 @@ class SettingsUIFragment: BasePreferenceFragment() { } } + findPreference("pref_monet_enabled")?.apply { + isVisible = monetAvailable + setOnPreferenceChangeListener { _, _ -> + requireActivity().recreate() + return@setOnPreferenceChangeListener true + } + } + findPreference("pref_date_format")?.apply { setupAsEditTextPreference( setupViews = { it.textLayout.setHint(Strings.date_format) }, diff --git a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt similarity index 67% rename from feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt index ce74728c..c6446c30 100644 --- a/feature/feature-settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt @@ -2,21 +2,16 @@ package com.f0x1d.logfox.feature.settings.ui.fragment.base import android.os.Bundle import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceFragmentCompat -import com.f0x1d.logfox.context.isHorizontalOrientation +import com.f0x1d.logfox.arch.isHorizontalOrientation +import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner import com.f0x1d.logfox.feature.settings.R import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.view.setupBackButtonForNavController import com.google.android.material.appbar.MaterialToolbar import dev.chrisbanes.insetter.applyInsetter -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.launch -abstract class BasePreferenceFragment: PreferenceFragmentCompat() { +abstract class BasePreferenceFragment: PreferenceFragmentCompat(), SimpleFragmentLifecycleOwner { open val title = Strings.settings open val showBackArrow = false @@ -40,13 +35,4 @@ abstract class BasePreferenceFragment: PreferenceFragmentCompat() { } } } - - protected fun Flow.collectWithLifecycle( - state: Lifecycle.State = Lifecycle.State.STARTED, - collector: FlowCollector, - ) = lifecycleScope.launch { - repeatOnLifecycle(state) { - collect(collector) - } - } } diff --git a/feature/feature-settings/src/main/res/layout/preference_material_switch.xml b/feature/settings/src/main/res/layout/preference_material_switch.xml similarity index 100% rename from feature/feature-settings/src/main/res/layout/preference_material_switch.xml rename to feature/settings/src/main/res/layout/preference_material_switch.xml diff --git a/feature/feature-settings/src/main/res/layout/preference_warning.xml b/feature/settings/src/main/res/layout/preference_warning.xml similarity index 100% rename from feature/feature-settings/src/main/res/layout/preference_warning.xml rename to feature/settings/src/main/res/layout/preference_warning.xml diff --git a/feature/feature-settings/src/main/res/values/ids.xml b/feature/settings/src/main/res/values/ids.xml similarity index 100% rename from feature/feature-settings/src/main/res/values/ids.xml rename to feature/settings/src/main/res/values/ids.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_crashes.xml b/feature/settings/src/main/res/xml/settings_crashes.xml similarity index 100% rename from feature/feature-settings/src/main/res/xml/settings_crashes.xml rename to feature/settings/src/main/res/xml/settings_crashes.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_links.xml b/feature/settings/src/main/res/xml/settings_links.xml similarity index 100% rename from feature/feature-settings/src/main/res/xml/settings_links.xml rename to feature/settings/src/main/res/xml/settings_links.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_menu.xml b/feature/settings/src/main/res/xml/settings_menu.xml similarity index 100% rename from feature/feature-settings/src/main/res/xml/settings_menu.xml rename to feature/settings/src/main/res/xml/settings_menu.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_notifications.xml b/feature/settings/src/main/res/xml/settings_notifications.xml similarity index 100% rename from feature/feature-settings/src/main/res/xml/settings_notifications.xml rename to feature/settings/src/main/res/xml/settings_notifications.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_service.xml b/feature/settings/src/main/res/xml/settings_service.xml similarity index 100% rename from feature/feature-settings/src/main/res/xml/settings_service.xml rename to feature/settings/src/main/res/xml/settings_service.xml diff --git a/feature/feature-settings/src/main/res/xml/settings_ui.xml b/feature/settings/src/main/res/xml/settings_ui.xml similarity index 82% rename from feature/feature-settings/src/main/res/xml/settings_ui.xml rename to feature/settings/src/main/res/xml/settings_ui.xml index 1dabdbcc..da43f599 100644 --- a/feature/feature-settings/src/main/res/xml/settings_ui.xml +++ b/feature/settings/src/main/res/xml/settings_ui.xml @@ -7,6 +7,14 @@ android:title="@string/night_theme" android:key="pref_night_theme" app:iconSpaceReserved="false" /> + + + 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 literal 0 HcmV?d00001 diff --git a/feature/feature-setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png similarity index 68% rename from feature/feature-setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png rename to feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png index a003b541b66cdb5836aca25980949e55e527038b..35eaa09c59c4b64cbe24e575d112524474edce32 100644 GIT binary patch delta 24760 zcmc$`cT`jPyEnQKMMsJ{Dxfsmh>C~|0!mj$P?4fYM?jDsni!B0Hp&1h0y-81grM}^ zJ0u`YX+a>N69FMW2tA>MkoUV8o!|MLbMO1!yVkkq{^816B>P*Q{(PR#_SyQ@XX{&Y ziAUk5k+18kXHu z#y`HP_QzJ^d*p^kk2wCICMmdDd+9IVAdor-qDU04avVud-{>(f1VQn+ zTZO=f+TCB|z(>HRUs2!#3i_=F`Pd0-=5m0~eQzx}!N-ZyHC*6B_y5V0|2+@F|MOe^ z+m7)6K|cRIxBRy)|7}P3zpCW_>n;CnN3cJzg8$Ji|7}P3|0lcqsS)^p&eVTy`ENVI z|2Ka5Kcx|qe}#ho)0X?x4ZF*7og0U9{b-SQ86S7mCg{BllHMh?BVjg1$RFLEIhRwS zB9vV@JUQouKe+2v`Uz3^3Wz}_r1`xmwNtyi{=8OT7%Vi4Icz59cu7u;Gg#ekXv9MR zizkZFc8HKY?Fc8J!Yv>KNnU=zkwq2nzOe(3u{3|T@M$X7v$p#&A7m12$r*6h>*MGR zqlKAdH)hZ3aQ@BJfSvY|^B1bDrytMtEwp|p(5vRCo&$02EDC0btQ4P99Ho%z13om+2y3W{PG+ABVUyy>Xz0UwU_ zK=BUY!qlCx|I8gHyXqbM*je|^$jG?mLN2IUcQ@GOdwB((d+{x52m7qgb-5i|)RR{# z@hg479gA3)v=tJ(0b~%|RUTIHsmP1a#-*(0`-H%5X!R}VgP_1Gxg6Cm&U=hk%yqRa z=U?P&k84oHA9;TPqmr>7I;#sFUcP6u}P!%5Lf(LBdLyqTOiiq9>`MnM$EFm zoEyi)mzGs(zR4B{1CoKIT3akjMd^aIx11%1{U1D6I?KTq;OFsVMB%b-6vQh~D7v}~QAtmCAlLIaKf zuyD(VqvEyuyLH;Q_CvKNNlAerI<~Uh0gU54&>p1*BeZlx*r0)YAx$YjPBXXyE?Uzilc|?d(dnsEo{W?|U^bgfL|8 z-lDsSPw1zz?m~s)K**5;(w-OP9$L=bh$}eteo8?ox|dIM^7OWolU3)MKia^&joJ_B zQ}EpUtGk3pd++wQT^_DNTLe2u;l%vCna`*Fbor_JCbf{g4iMpWZLEse8NACau%O#% z(<<3k;b;>78K#q`YU=`nX^~jc>89vAQ<1xtkn;&1r2BcAmyWDm`;bBPGiZkFRgop+ zP<=7H!h*uP!eM2R0{J2gqaHU6$3@#5s!=aP$4&9=Qlgu9&b*u%lb3t@I5xku)uAtC zu)h2a>366v0%+o|G>`6kfm~dTePN27c|Lmmol==iGik;hmZPJszMmGkyKQWx%q^mg z^BJ#VlMMAbNa!`Q($V47-c^l!5#MkF3>VwIe<@R^BIW{o=kIgD;9xG5-Ircnwkb8~ zj`FzM8=ck5I;VFKRk^YjoT)RF(yh%ys4A3ufMsS+^<*jX*uNiT9lFMNQ?;)$Y;{31MQ*{-O(|Z565t zMN5!!3X?h7&9}lr(4Vos;WnIHTpE5=N1PR<_>4OJj}p}$hfTba^vwvj$oJDD*(Lsp zIyWA}DPpX}8D9mQZbt?Qe#6TzD6Gg3PCNL%fxr9JRc6?2PF}f@wL@=c1nGy%TpC^@t*5h8Qhz5TLkan zYF%*0pH*?;?w|#pk3z6WRlw!PR;(11_J2;Tu(N5Dx5~}QW6zaL4l5T6x+GHUH4r1t zfE}Ph0bs4e5rostT--bZfjU)h!>?#^`y{i}^B-{ud8rnsPOT_2BkVA@V4s=Tb_~Oh z=&oF}GJ9|OyL`do(!CSehkGWlAmrQ0@&+)D0G$jh_Kcu%tE(l`^Kj~B=g!dt!f>-e z9Xt_B1YZwgy|sxq*JriBuxqffF^paxms+;NoKl`2P#j+OBFdZ?QH3{%k8y4V3`u4r~PDqBoKl3uJh~=!D=b!Y? zA=bZz_F6uA)SstlVR1D3z|CIq;9s{#c)s=boLcIgTPbk0qoh&bc?Z|X@Tau)dXGY@ zYUy^%(atRLZ4vfV1=a@BG?H=nP6|Pv&VJZAGiTwYY*FCKbL~`ri8x_tt?8;v_D$RQ z_J+E(H>4jra%&wdC;&RzYj>;5(cM7lS*2VU@1fj(O(H z&$_b6k#N4hjgZ{zYM#f6>9-e*vtca?GhG2U7Pt)&c)Rk-SRj@;&cV zVCP9U<)(*jxL%q6?9C5aM~Vf8BAqs1l^F&Bs2JmCFz7vAeftvqYX zyh7D9O=^HnI5YXsf5LcL+4QSf-Bha=35NkMI$V4!iz1tf9_QF3SmmH?7|m zbqtAIvhkTjpJ)+x`nxEHp)-T~5*i6Ovs|oO}`~&f7SDksG-bvuMsP zE!4i^Gjw~5lp(Hzwo{$`qLUMAL{Go!SAUGwndx*J#0>RZB!@vxsAE>`?VmHld@f$e z+WnEVc5qU*^AYN3mOgCf_&TXZU3dUHtzfEGp4{+!RqP%mhD``jhY-j!MU2F|fvksL}?6Cf5LbW~ZN6j(D z-*%Oe?;thF$ToFYCZyC~X0Np5r{lzN$|4(M;||97E?zuV4@M7AEJQx9Egk3Glg*sP zpJN?NE5utS$W?rV4k51m&~;YvTw#TY%z15MZ%`sQy0q{$Cb6SGT4>DRPu-(anz?(N zSyPt!?E2Xy+-F;!9j8o{H2PBH-MQ3lm^l@3u4}TlfVc;MJN}HGCwO@}YPJwXA5Im) z=@)IwlDHq6E8D8~C3UU+da2GW!QlKYm-(W%xc9E->9Rd5ti3sJ%{xz&lxANUI3r6l zoNN-pW-Hh(<3@pagq0RXDJ)j~t9n`KXiV5^%LM6VrQ-7$Kiy^5Uh-!y?(dGym!n-+ z2TLTiOoiihR+* zQ}*1gQnz8IW@8;b!YMY9b;@}NhaWk0czx!nVLq%vS30*EmTs*&xTZz=;RH7_EgXI( z8cjpg7jXWLSm~y$`_%5z8TGzXfe~>_zjd7`G1w))wioq>M7L{>f=g#H%NToIN^($_ z$Q~@}2fkH_zpFvaTQp~$r~WX<@z~`Oo1Xy^wf2#65k9J#UNm|QJ3`8RLmN&Zx2(OX4>~VV5*h}RKYuJOQC_-FoPIGA z+Wnk;rqjQ*Tsb%qzy&cFYezuLckWimI`wccZHX4H4zcFfAz=(nZxWDKU%zDYr zPeYvI>EYf^bc!+F)*|6FhMrW??cVoQCp&7lf=*nk<3WgqRFc<@vMEvcv?39wfqBH~ zr01?i@TXyGB6sD? z1-B#XcSkxhXOHUO_wxyF6sOPDB*YG5U81Au&LBDunJ(K9^Tb^HFb*X3ft$abxf(6n z>0&i_-RS^C(*TF~QfDktaFMg9zIRVp@ruuNAHO7@{_^Ax>c%|sj@w{~y!u{F2llWW zIKnfQME|_2PNYg;p+zs7HTuzQ;kFl1IuHF`NiTXn-(&V`NBA*^P}q}_5SA6x@!aVT zomM|7*KozDt}0q_yzXOmYVcBV zWwLsol)h7I*pRvJ0X|SBeWaM1n4-gF;6aM-ovVwpuyR@9;#Hn~Q$?5%g--*&iAAm@ zH_Cqm8*;yO>8TW7pS$cczPq8-D30zb9vODf!bI#umJL)WhE&={b_wq8hM_|yW;f0$ zXTM!>Yz7tTb(U{*+RJCd>O3-50lx|y(t_K_10eBrOa~uWR6X$&7Gs~L$-}!ZW5UQP zXQh;6ywH>63xEC1zSE=Ha*?8&Eu-i2@a?+{1WW>F6~b=OxVJBZ8;$ z%#&n!0vMnO{q|QCQ{j4NMJLDBB4YoVY_Sf0?2BS}9-GK_Fz%}$YyQ(9Q$+b*92ilf+48S8(JG>WAr3=9+1&_9> zNvy2auRU0>ioNb60MS7Es%ASiAlqxXC4PV1+H2Bjs4oW8i7hp?Kl-J8oBh&d4eg)( zQhYK}r3LN`7(VAtYRW|VE0{AN+LNX%>hIH9hqB~2D{Q6wyD4XY322qNY)D+~r&>r* zyeK#wT13%bVSQ0cuYiL~O{QLj2bR_@Mr6=8uL@G!|6m zEaanPn}f9gqVRXqJRxIRz2)+6q)51M98%LkF46qnP^F3M7{s}U zdfmhcikC;0Xw^PhZJEcp6Jl*501etcGD7j7$Hot1mFeHZ5H*zt#(J#lyEU_cnL? zM-g{J6T5(z10AW1z+qKaY#i@B=%bOCRqR!#?a)#r5W__T)hT6v>CWfiZ6C6_^Mrml z>i~3iH#lc-%U@pdK@!>6_q;9vj9;nM8MCM+$E^?#7g+MLV`Fy0Fj6{kio`1;U1?7l z#mGPivdrBf2SfJH_EU>P*p(P{xd{&S)<@;t>XkiJll}3OT1u>mCl0MKe4}@x2!|@&0(as;S5nIuoH!X{{m;eyLY?VDAwZU z!QDxL$KnP3{cAgiCog2wzD!8Tw-bh``JkAwXrZP+z!bCLC8n+TcE4USrB^7iqdGEJ2;$|;it+)ynN^P3g?M;;il zzJ{N5U^LxocSpjJ8n@uVdGAod9ls3&xHMr&DfuUk76gAKqR*iE-AfCDzE58f3)V~uYAofCq zN5B#P-%kbry!?L&d;WK#p7FWmC|kqP%7&x%1J!b{~IFTHV}Aq1UHw(B~6mpmRu+yzB8DpgW5m@T`d<=gw0q z6{E>gQp(Mfi@Z=PXkp_g{|&O#-SSLarCADB{9y@|LQsTwWWh#SMJ}V+ZZy#GJ=&w| zH{Ux{>g~!H;H=gKPTs!HuFX#%E_&5mZub`TxOqa(4m)iuVd{tLTy`Pg_72N06PjTz z)#}gapOEDg2gD->mcHyrT+8|Nrr0&8_`z71-X~u%zbN9iA3$l#W}s3~V?Hf9l(h>w zYXqVf&r^rRAp)1Y8Z#P!R>UF8vq%)WO==#99{%EL z69^v&$QSf$fX-!!LG2bGlyJ3nl{w{K0d+v$R0@n{s6H&`|k8{ z@Blc^L_yB7Fn6Sd{7HbKE*WN?%qvc$b3r~w!SjR$rOHDHgxd8EM7*DW)!EFg>Zx*T zxUyl;;r|J*x61Sqxb$EpxKg{)d`;#=x{-B9IVYqHil+8l>qear80X+9w|iJa`rdia zM?Z0HoH=@#-=%J`VX=`vM|{9C@ri75L4jh1Dx`~`W5HoDI3=M!b)ARv@ozr`-)B1R z_!RaK_i(6*KPQF}_%ZstlZ^qGrXk6cw|5i#1lT9cx%+-I36-@|#B=&UrCY)|J!>6w z>YkiPM&OaqqJ~Q!p>x+kRJ+_lnus|!kovw1X-jxBu$ zbPK4(M-~B7+b>#oxLM~$>jrx6cG5A4fDR!_(GU@_`=lROsPc!R&w6i3=M5`e6D3z4 zNkNAZsv&?^;|tGlsQm^mrfh=1t3C13uPB+<7oS{oo?oAoy_@({_C%KE5A1lx8|5z7 z9`Ds`u&Y(#1ndyneJzLg=(Ws?Kg)wD1)ofb^=T8+N(K?&q*{w1hn?qrCY7tN?r~~I z635!s&6;e=$9qz|_fTLQ0{flwvcDeMtu?z%{h3i{$IoAsgNX+!;3CHTLv5gjVrR>T zleBLy+dAl-zNexqMDGZO$HEik-?NBVJIvaVHrq5M3rPg0KtQJmm41=&%C(!NW3YKk zTc<#nnT&$p`n?{HZ-4Zx5o_k!`lLQ3;xl>J4?4T1{hmt^KNhO|jf6-4gVk2Y<39BD z6@8WawKyP*bG^y4oH-D{#dO76U~uaF-DF|z2v}v&z!Rswv>crmJEF7uf zY52HanFlz5uy)*(Hk@>pR=-?;3p;rA^X`5-i)n5Ie4VaTJ}@C`%^bJX5JWF$PQwGV z)1<^g?M*N>J{duHIk~4oKU}WjhYfm{LDi6W#|M=MCv^L)1&z3HDBdMKTk1Ypv=V)_ zujHn~=ehZD<>A#JYIY>0svpVRzXDN zS@@uoEUNgsSFSPz>XV(Bofauw{ws%u#9|8B;xBrU!kM zGw|KdN46xtk!%~k$}G$p>aE$1?$ku_C#U-x-|FlPDiyPvRw?%}! zw68G>3tagg+@tS=Koq`bki|YFq^oP;dhs%9*0pDkt)z($E3G>6`CPq(}O|N^^0dR@dcQE@RabEGD(X!Q{?n=j{*Xh>QxqmSn?5J`x zO;dlLzr}kj0i_01LV0fB=uUaPBx>5rehybnZHyiim?ywsSmB1Vk|spR)7rvm$k8Vj zfc5Sr$7L%t&SD?RQVkuz;tLHa8Rj(?z>p$SN6{C!u5r~Cs0QYOqEuBnm)SByZ6=aE zPnKT|SxcDeNpt_kH>E*d9{p70o)vaK^st6SNrA}K&$|5G%c&@L#*My|4z+CW*;2!d za7lvsuxo>sMI2EA0^_w8Wfag-2RvTWj#V+06%?5I#LNvC?|! z-J&Deb}#R-d+-V0rKpnhwmfg2bE7d%)`+32cnTHSk$FnsA zfa=vVn1 z+jxqTB;*WvC)i4?!AzarVQXJMm*K#$Kn-{xT&~z@ChiUWq-IWU zB)^?wi#I%0D;oI9lfR@^>u+~S?I5C6gl@DK7>ANdUN@#x#SU6$JaxJ*obztmSMT(S z!Xn4MoN1z$YKGuO$XV~U^h_N|htKU(V1^~4_;vq`P{}bxuZqwQP9_DLH*I(xhI1(5 z@9w=in@6mhOUjpI$5XmBJGu>g`lAw0!@LIoz$HwbNLL_8xkSB1j&_0Fs&l;p-?acY zwD5ST-{O%=KZlvUl&{tZ6!>g|kbAn%z2N(~h%ev!tFC2a+J`T$ z_b55%+Fu;L?&WHXjLLvt=B(oPMjD8aO<#0qBiY{Iht2KU>nnQtRu<7jV!P0?67GD} zAV(3rOJ!A|LOtnGFwZ>kMR=QxK1Fcq?AS~rzrG0Gmpl)Kh0p#mHW>tP1>TZHGP%%W zgmW<^R-FM*capOf^CNYsOtTTIynM%j_xX~74JYq`IE&KksTEcTzI82;%coUUr66-^ zMY4a_Jxrf|rj`Q|eYhYvDoLXF-lk)d(rX&t$g7BGd&X8hz*YtQO3z$(^R0Ik`qUCc z4=jE3ClA84AiA8$)AF5XWm-5M>V7_CM)xMp&o66%nC~@2e)}T?^)A(K{5Ys=&%33_ zH~Pz~U+pRiRKgy*tyA?Wy=BYxq?Anj;aXU0R||JY1U6H_voS6K8JgnKHq2$7aWBWs zY`CjmMc0^HmNbWO2qceO8hzBv2`zj z^l;`zh#35-6u^iSSHryHN$8yr(nQN@y)XJ*$VfNFGQlhKEvslsG!o90q;?9->%SE9 zZ@G5;(H~jy$AI7VjgSfxX0b7`CE}b_wkYYX&mL*`%y{xU-7-mm4uLLAw=&jk{O3}Wlm3O!8yh* z(l;iXd0u+%`47t>G#vy`3go_DAdsWem|WX8!b)5||9<1Fy91FWqSO&ZpNuFKx^BaK zK$Qp6iA%xfsYlO{yuYuc-O|l}P(R*5Jqfd^UwX?968mK(O#NWiv25}@eaJe=`1Cye z^^)InyisUFBx&Os^#^$(zfIk`@c?M&H5{6IAlGxqi*l5^ zGxI~5q`7L9_=$Y1MW(X2)W6oLibhB<-{OmHoQjjZFYB39y7)-xKFDJv(s#gr_a5lE zxi4<@^j8HJw-?a{JkGU>1Q*guA66}if;+Wdc<}w6!H-ttFlf{XT%uk4B`~mfnO3pj z#CmESHE9{TB9DNYe1AvfgZWOgfL?;IOLjR{__VZi+<5i@HNQTuFj@3r!YWG|fSd?? z2iw0h7Q%peZ6>IwTaR)jfy%n}2Cw?*0$FH+8)?-PJ=Y(sP-2p819$t#`OdGkqsPsn zwU#pK7wZ<2x&vMwA-!vBo5|dO)Rd9t_WLFldSqz^P*C)C9^>j5nKxwFUiWknyoU6k zM3Eni9;EslSgM9AQD-k%k5G?B==fF?Dm!&g7C3m`B#scQ@jYZG@xnj=oJu;h{^STr zT8OrA9b3|Q|HU21ycV>$uQz-sY<5P}fx;NXi~aonLoaNx38vnF)+9);3)>*IqnkKq zS%JVOq&E)S8-P0qFU~!_rFD(U1D(4Bj0*n`oO*GmT7nht)1qTpQc(K|APCO<;R0nq zCfkd&?XFg|o?Hi414AfY3c&*2Nm@SIcv5sUOB`x91Y+Q--vlWnA$nc(OVXYn*k*(F zNwuTX0b8mq5garXK~rI&I_P!%!IvanNN@+(CD5gbJENe6r+Sf_WJliuae?l)`o+7F zdzVX5w^edE`gAh4N zbK&^|Fg;eQcX1bV89^BJXg%n21EA+bo!z>REMhINNgyF8Lp(?aTx(TdPD23Pm%Psd zMxHu37#-RdFbp6i%bxcN=rcFqRZhhewTtz3%Vm+Iw{h^;(kfSgElB!jr@^dX2kgU$ zos;3%zV8#DJnck+zwr64C}jfFjsmweQ5QZI2K!%eXn)O@7%;rNC=|!1LogNz7gm0w zjEBye0bLZn2j+;-{E~upNC~~!l^rntl<+!X=CW2J*V~;` z(VOozVGW0O6ueb|q4sp-*9JcrkR5Wb{vBuhPcWlh;CXv-1Z0H&cPQh(5pn;G75m>t zibd$X|9b*Bn6rT2{73#b29&4>W_(nel zu!QSFh4sOl2F`m{m?!8Xq);PyE7lYAG14m|Lo3Wv^zkfZNEb$+BT*4mcZJLiH;s4a zd~FbI{5`eUA5gQ8dLvUl4@4Jxjfs_F#SAS-7qQQ0hdAwJRm*HItSLVT0S3FNors}!zjft{joreBbdJuh&X}svpg1xb{!qBGS=E^c@KESbIYGVD#J!B zKK3~<{_`Gdeg;Q51P_2S9_{)`{lwI4x7UzYU9su`&|0R!7G1x`ya9Xcf8eOwQt$tR zquzj?0iNL5Pe@-o4L2)6s?X5BXT?JMz>vjqYn0q9YnodLDvMg|wvpWzUKpRh#BG;>ZMQ+07a<*3p?rd~BpOppe3ju=ph6G>C&F%5-dEv=L z`vXQmZ@RCiSyS~3E}pa9D=#itmOsD>M*iCM6G_Ef*d9M?rHpYRk}^qK+SkAhw-ut9 zHFE_{;tRv|%ur$^y;0#6Ejby$tcIJvl16;(UXT~)=1jd@nNHhQ9Ra>l6Kpnmr zKmu^1{awHSEetk_=sd`)<^%*(m+`*BQBQLLf zHfL&iaD8kelVDl8_%&$wQHnV?LZ@J%;A2-+8?!7#TGr04Tg7TS{oH6qu;LK&*THZV z)<{e`hxyn36pQI*|Hk2k_C)zlAz0c@5phCdiLoOEU$NVBTIIg`T(R^z=tgZDyZ_?* z9CD<`XrrcBlfni#<4R;qiVf!38J6%ip{$dn$wE)gvwnzKd|#Segg1G|79ab+UX97y z17nxGP{m)D36i|8nH}ZYSZ=-X?6$Ty*SCKU6hYPP9%YL8r8&)6s~bFhC={9CiU4JGq6 z-{e-UMgsEu{$^if5CvLS{w!Vu#_{Jn@9h{ zy?>y9g5xH`?8}q-(b(|nE1sQ%o4H@%a@#hGNTM4QjKqwffrqo)y^$18n}&vO#sz@B zQzfkURe$H%4*#@zSJ(tSP_`&=J*Oa%_3?q4%Dx))eYMZe`yOR2zC5xi-DU+LDC;sX zlp}4P7CVpx!5AyMLyXSb!;Zl8vCTsE1;PtaZZYng(R0QJLoS z8S71^T3j8?QlgvuB=qS9D@WXp)xl9_nE-bnx9r1RXn#q(4@K)BXG0)MX2(EVOOK#1 zOsJ|Xo>Cd%d0wdF$`Cz&6CDnK`FHed+rK;Hx-%~?)&ed_2xuIF--x;F0%Rz@QM)RE z4OTHF--GBusizJh)Y(abgg%2BVS=~bXIFijQ88KLZHzThCof+jx{sl?ZcS2puIA2D z$*o^z@FauM%a~c3{b0i#W6AAs{3>FKR&5euHWtzS#;`4FebhDX7cc{Mb3bI@EAqrP zZ9mFS;YN-6z^_I5&I29Gv0g(aEbpE?%EWJfV?7Foq_zv;eMjDOoPOz(MQtgYTM!Z| zSR^jA)`X{9Q?rWpu>Im`-@dj@SKbuvF)T*IECs4wbN?)S6Acf%l8Lm#d|dLFmh347 zy?HGlm);gB*y!xz&fue0l_W$)`v3Kh=td+sCnLu_N!07vi6)G6rN zDHrV2&;T$z1GJc3IU$5CV{1!tjql*_=8+qjIpx0nm3m*Fjt}zw6_Y^(VYI2?VmKV9UQAwu>pV}f4SnFDM z;oe~!T(}pN(n!gdYTi z*^&-jvajMZv|`pzVVHR=PyZA~=?R-E(lp>G=1QB%$bCHTGzw_qou;RX0MNvXuB=V@x;CL~J~lO*Qs9vkyf)N-Gqc}8a{J$wIwDZAv<{p0YgyQ#>Q1h> z-);8EiW*kWva~*Lhx%il{(OvDtY0O1t9JxDJLIKw+Mm+t23p1ZQQud5p9xred%L%s zE7<})z}7a4rk{j`rY(;KZ*`XpYbpke>r9KJ4}9A@2zkHvJYk15c59vL|GGEjy6HkP zCCvsglKS#k(oa7|-bY|0mBrH04eCoJZlhOydXh+)o1%FyBX>%9VafC^0LCY2Hj-^S zA2O1bsb~I(0-;DK&wWxqs!0y;RWdCkFN^RcVy$`P%>AvciP-vjtxW(8<%-w=n2^QGBBj%xvU2Hk zFK}g7CznLlcD*di1fngf>sNub=ZdL`6>naf>TnJ}P#a3GQ8zO@I(S?*Tlp_WhTW#q zJeeEifD6^?Gl2c5vEMwK+;FPbchUXOydtM}Q zxQ|wG_Pcl_9+-F@7I!HOkm7CsKJ^Y*M(&n7Nz-rt$NWw7EEG771eZ_&zdqad!XV}h z!r05S(D~b0?=v@;V%KxxZx@$px%&9&ATs%i1c!gcM}hB4AZiJH-rT2@q3)_^!E+mk z#~vtY#NZkq5{5MeBMZhDQoq&Zay@Cw`lIHt@=`N znQNxQ) zfk+>hk0c0?*THDHgFP=*g48_tQX3Ocg-Y*8MMZ#VH%VONMalKtmm3um3YatSI^z*8 zE92%#FjQ0l`Gl16R(&%y>{lN|>M5B)BGvia??QHnomMSw6leZZj7)|$a6P7d%#o2( z$=dU09jX6DSgB%DTBluS?LkMn?cKz{Nj%-zKX(tNh-rzIGx((axR;? zoAXtng~B1&HOO+*OQ-U$tPE+FhH9=c_l7zz0oB0!q5XLqOMOp`@z>e+7Yp40fC~uR z22x1Q0OZ+yfkjRHvnO$mz^%?9V%o&#e+EVcg|>T0F#$q!*Ay9?D>WrbG^`L1tXtcR zC3*zI4+wn~!x|gNIvs^KSQ)ea{Acj~w@C76@cKx;q(uoThXt>8a;&;-7eZ$CZYvMz z*@5b>jQ{6*5@^tB>Eg*R%?WSJZ7nUz_Ot8Ec}-?x)rl7;)YG zf=#}z@<{#MXxvAC}`(RKg zgT!ibLk5S_r4~J3v6I5*Nxz>?kNfhgmqrw|5}KG1*cTM@d|+~vI%VoY{{-^?^vNFhNq0}od)4d^ zM3AqShg__xw4KgFwMY?r>3Ew-6vo@du*lYOX&*>#MNfMu^LFGme*+!iI&|!XNb~d$ z2Cn=d`6ZZ>ZJ%DeGS&iKap{hIa#7>`@NPEFAT;OfjWtacTLlp&mk@h7A~b!CUQz{K zEt?*=$)lv8022}iHFK)!2ziTuYE4zl!>}=Q)oyMi2c}paEe93Z<>lTfy4-l>!d+4h z>cni-V4Y7a>1V;7X~Nb=YHW~z8r16CtQjBaNsEm^7e_68nUwRLN0oIpJ<|^LT3L=p zn`Y0+ckqcQ9}tZmSoEx$4Df@`2ZpgXIwka3YyDF|ft)%x(p=x^3Wf-GD0#JTiZB%f zytBtNcTauqe!wosd`b;XWHkj(4fShfc02HHw|s~sgwH=G6BhCPH$e?s_zZDGLeGiN z&?^n-D7*3+G z53%zenW#-2p^o)DF@Tr!iSF(UEi=1gG()+4%R}1WMO9qknOpz#N!GDpc!%-lVKoL4 zJ@!Edgwh0!>SE80u;(L|*VjLxlLqCGu2kRYOnU+H77*a$m1uHQw}isj;XjQfKGAly z64r`79w9T`@^M^hP5}8Se1{+~pfkLgbwCJtZ)?kA&0>Rf3^!}>L+El;F_J{^M|E%H zX{Qtk1q=gZ!d}{U8T~r>LW)IVzm{3E)v=;db&WN}E%ke;i$g3CV;Yy`3L>m_0EOMN zNbYY1a2=f7tMv7xWwVXHHnD|g<%hi1DxmuP)cYnsk&S<*B2pyioJD41Ncj5}M_H&A>7qU%jE7A) z1o0B0J!BGyG?27DU#0b>=%>8`C@5N-bQg>6!s3MNDm4M7`?~Ej5uj!S^IfA@a zD~z+}JiBDYq9L#A%4{}|x=>Cf%a~y!6!4)0;d3A7Y&SvBAh%+`@|=9+-7DY%5FuA7 z02GP-0E+SuLU$zia|7}Uh6>)utJ8!>jS955?=wOtlp;esI3w52gFI?Z2nO}}=M2IE zuI3O_+N5-sIPh}C%Yu4;=lJKZdcz1o0kdbjw9QwXO!np@2YpgQ|rOImVp8D?fUWNnM5BMYH6yE*`yRT$}X9^_V&2HM}*rhO9P zwB8PoJ@CwP7_t_xib7yIFLp$gBI|8ZYUj4~Zo0`bqkMVv4#u~`v$%+6a1(VC^~W|L zH#M&Pe~e$`+OI48Zr_1FbhrM&v1gyqwwqiWAu|;Qcl@ttycEx1q@`9Zq8Jr~#bIO7 zQ%5}PY=VvVxZ0|dRz3W*w;clSa#~_nmV)FZ(LEeJaNmVU*lW^kryAe|R;%2f*Y|4M z7$G*tdO_v}lrK4E@NCD0eF5MN`2fs#U(76)Md*$z?|QlY+xOMg`888fBL%N5F91fJ zyNk6_V?o$hquEKKCph@*(B_$oqbu)!Q3F6>wF|Hvsx~{`tVe{=fpfKa&!vz=oLDT9fWR)(xlykjA&Oivu2TTr7Gr zYv}!!X+^X-b3ni)3QK9As6Y%U!2hYfJN30OE-v=rcu@iS@HglaYmBNLi&>Z!vc&_k zVUmXk!~}=@cGi&d(JMlkyUJO?UHlLupG^<$v`evY>ztO?Q7L)g!uf3ud3S%N*N%O@ zVr|Wr@`}c)&xqW<8}hvviyhh+ZXS2_OJ5|dm~mAP12C=nuSS2?G(5d7HK^h{$~(IiZP&DnWat$G9Nk{akPFmlQ1iw*Oh> z*nv0&b^4>z{WAK#@0xDY?#h>3 z`078Xx=U$gv6qZv+zFkPTbh=UI!)Ye3IB>x%QZC@j^|NyS6%&5tBEe|==?%M7NFqHS)F5Zkf#f+;Qg`I^~v(`K}OyCY(;M zwk7xv3?5^a5($zD55}GsNSUS=you9x(TNq+%L<{DO=ZZpEweyfplDY@cAvfFJ$V6E zS4R8XkbPm}o9y^6iA4bW$NO2$$8Jox*5|6OKH)c!{dw>8;!gI}-Ui!L zl(jx(jBb>TwlM*XZ?Yxak{nNe^D2K5JwR$_8vGd&fpwEo$85q+C^AW~=SDKPnxK%cj81>CO?bh${RUzyb38wYOUS>j#N z*K(UukStn}>YTmUC!>>tJ?2rtw3A1r489)ZkxRB0hu3RiN&nC(^5_Ds=RvF@OSGRQ zGVldQc3}!qdkqzPwZFajttUWDwS4wN{jS+0LqF>cf~VV~OB(xr(7#c6Q9(nDnbxbi(JWmM z5$nh3SZKwlZuG9xWZ=bUFOl51Zrp0>Dvi)jTknEWHau4cE<{Sbk@q6Asy_}@sjf}l z^42345i{CMnd{Faa_;!~QgTlW)BQfpHR3wTu8baQ zDL5i9Z5G`U>TFvY?x@-*3a?FX_hQ{k+wfGHuj?%EkXl)o4r4!9YQpQ4)JH^~^kU6z z)NkOweawh#3URD%pCT=bMMlzGG=QVpdzLHf6@_ROmrbk3oK#fC#;!bt34{p!pk6u| z?Vp2@_TPJz_p|83ZxqdD`R)Cx7R-j$P`R6p_baE{<#FE4A7=#4u-9&R=g^8{c0TAM z=Xki8X16WU78kz1k_x(qe%^0a>#-t4W<08=EDtUhB;h+zC9#F02B*x}4MinwLrWuM z6m_Ldl5AmO8ylPHQ9*_idfR5dX){=!{Tc;daD87qxA=l=vjsEm5qaAGA$6xZ#(;B( zVXR}fXc;@8HMKNxM+dEVNhokVSAue$;hq&R0tSk!8S75juvzi*B;-$%(1X0>JEzEBJkKdN=A~#ySs}5ds=w0I**1N#ml>h#eRG+q zx-vgKYwiVq?@)-wd$;acJoKP4{C+M=@1nhgFHIHaildE=#oV2^eVAEV*{I#8btg}( z+jLAG?`><&j@1yw-AY zc9IRIc6vKon-c8W);?8A!o)#N`m?b<#zEcUg&5Z=CkK&Qx2YLlQL|I7Q!XFV^$dBj z{!`VAbM>CD<3@Y*?9yoDaEoK5Z=dJh=6D}$`i`R~ly6n(rPQ+A#IX87+a=GWr>^DyaOZ#uLZv$oz z2n3&QrzEl_NRl^EZ=8wJ&Ls4GU+U6?F~ON!;d?Vh!2-{u-4`E;fL92;*RV>y zH(iwiVAu3f;#3#0U7vvMb}p!$v)0d*_KTfKZ_`8PMkxNTI?g>V>AUUYHY>MowQc6g zJk;8rnz`ki6BPv4)68|yOhr<}bf%VyW`!uIzsjW@VN27}u!Bnl1Wf`IJml??B~vs3 zkAOww0r7;Oh===U-R-%b*YkS*{{44xUGLB5bA7*GUS9X1@cI!u?(wd-tn1gadJ zuI;{u-~~^r74_*y5ePlp2+*ML98(^TUgq%JriZ%s`BdmIj~^R83@YyN{HEa%GQ)0M z+89qZ5hLp~=6}RL6)$(tqJx?IOd_z-Qu7?^{51uj6-C#c^*;)CHjtf zlHjYRuB5>H2&%BPVdz|vE}W|g54!?EqfopB>he zLOYe217Z%_cP$M=66R`Z9DHqn!td&3M!tf0%JTLDM^5RiouC97gcSvA9>f*n>FtMc z2&a}cVHU3is&-(?h%!m3ou(g=(ysiBmX$P%QX7mFg~?G_6LUiBgPjXl)!ZOI1GJ3o zn1Jjy#`=%pQmn5iWt`fbAk!c2a%kgWyhF z$witLZF`t=I>o^BRMuD%_=7hq;rknj@o5JsHGl{Mc`o^oA+RyFB)y9yWnloSV{oym4u~ z@R}&$&D``tk7mVDJ==R?J1hgSSXO+z7*F>ScdTq~?^^)EKFOfgs>O=fW%~^F<`NOOi zupjs@TL(7l8qY~|hg53)5t|b4gqvegeN&69G3H}Ue-hY7J9=-^D6mjYNH^y zDI0Fgx&>%BWIw&6GnH$Vruqo7{YM$0ujBy}U6^q3uM|q4_mHM5XZqICgLgJdEe)bf zg&-U%D`!AEBk~Er!_0*_sBfw~f2t0WuCry6)ziO*lv9@o%RZ=fvoFnvEk0F6LH8NP ztIo&y^Y!i4FLsX7ouaU>dSBw{09SfKljvWC%1-Hii!WHVs{mH$b8$Q`8PQ3HwE^J4G2!RX5`;Ma_rw1%5CRMV#~bl z-IA~A2an+?+Wt}(=cSJNjN#5HY`8U#R45aJnD^Y&+#0O;eXbV!a~^>62XnxJ{dnKp zJ{b0GxA=kG0s{lVL-}4sR${`Z)x4N=rxwqh9D*gUJZ}I@XFOf_?4_zbZR&zvuv_4T z^06M*KS0W_KRhh76T_BbTHe{0F6tMoOiA1fVIz$;|YLORL$>5T}bRoqnAel>Bz6 zTqXZ|re-ZW%RYp(*~A?OI2EWSm@d>_O;Gw|h9pM~=(g!2~OG`xC{x2q+^kYiIg>49zJ5^OXx)}Soz4=8ARF%738k!D%*2VWrVHGx*k92l)% z`4!G?AkJ-zjkBvR^*xF=X&zJ#yphGb0zoCBgOes$T4d=?7*>bKd~=9gD#rY%VIj5n z#Gh%YL6$z%E=brYJgK<%1SKew{226OAX~lz;d|n^g%0#%bD$r4dYZG~*YN!(FK42i z-UV(cqkN+Bq{{89ErnzYm|Rk~wLo@e0CTNofe%&#Vgbq79Z>Nc0IeAj?c0!q53lN( zBHay3CQ>l=O;P1c=xH~XMDS0cD3jZ!$}S_V0t;|PpbZL#*!&rhF)Bng<+=`RJf6&# zsMnPTtSAh*?hTvlB+4(2cXR4%2F)(5F|(~vm^g+ zfB^3aQoOsjg09unOC5acz`(ItC{4H`5Y86W$ny7nl6Y9+PaqHMgoF+UE0mEC!|J>Q zy(LG02y{E()!h%whXX47_qDUDJEqb{cx~PhaYuG>!Sskzr_cW;_)H~mXv%nYCUHLX z%Jf=l%ST)3(XJ7xBS2*H{#%k?D5}WhLhAFs`l0dq`IPi3Xe^G81q)a|KS$0?TAn1d zriF9ili^j*60FFL3pZAHGrH?Be}e-T9wy6mSWvyvzErPMyO5^O1lg?jrIVg@@<^#? zg^?Fqw~9;V6?+9V!N*08+|pqWCf!xFIupSE2+itHY=9i}gY=EbQ-(8HKzh$1vO7tX zSNwm904i{sqPZl~JLX*zB<`wq2jTg2%B()g)V{FlyZQ}jgIE3i-dBL?^vk5z%)Ad} zI>9b?x5>%83aN67+q66l1GISV=kPv=$e}z=$!$ntV`ExAf(yp3BC);_`T>MMReUmP zf4g2j(RPg8)bC}gyPiTsjth=>#!(|!Nh?P}S2K<=o1aw^EH^)nI)<9eu@WOMYjd~` z4PRB+Xa&NJaKq1LnG(%kYV#E!7#{|DD8k*}+xG^9;GPr@AGk*}&|W*=iDIkZK z`_NRiCu}%Ngw$g?n`PbYu*n# zUcgw-$eSbD1AZ4RdX+h-Z>6#e-+o_DslA@OZQ`xIcCkuq8+8pnlv0x6b$yFBGiVHX zn;{QDoy$q~uW%|;c7#!TB$5*+7;=x^Epo#&=L^L5#5bcVQV@0cH*~|s6=PgMo4d>1 zZNH1{?c5ZtwGkN41j?UfdPQwSH*2Mw)*bEfE5PtS-4K5|yd2uLx`}!i;rFKpQ>&sU zAIontR#**FWT2)|x{Hpncg8ZDepSw7+jIa^_$t^Lz3})Rvuiqhf{YOn+VdMisV#H} zXd(?abFgiz8lxUNT(_FHaRcG0rS17(Ng7O_IA-4n#DC0Qc+@|V8lm<4J_g`B7O%H$ z;S$A|50x{qSKm33Q7C=^UbgstX6BIHEXq?kxDJFf<(Ip%PPjLOLGD^-X0RWmpf=r9A<1x6o{p);i{5iB@`pigB zYx8~^z!ISj84qJhk6Qg8U6FoCXhC&J6kDpIt=#V0CIw|GQYd9matrT>*Ys>4rR-fs zPK#aQ&4fe+H^{3Zz6edexdm}1Kt(TMGo5Z~bks-kuY*k6HcXdfEf@?gG+pDwHeHO*=8eJxITs#>d# zV>DNY|weA-splYQlF)TH?aPlbGOsqB+sO@M|iM94_&Z~t`!f5e3 zdQ&KmsT>eNdhwn3)nx+wT}x3rR$GdiePipN7L(@m14#lL2lTW-pi;!2hc2U4QtRw# z3MWslE5SIzlM9KLusVgCqem#NJ|SR!ss$}N(kVth zhwi6KkD|7VJd8D)VL~C?~ zXr)M3K5BbVULSHeKD^vlHae4c&&!K%oG^QXKl22J$`NgxTkC6`FV>6K=CT_ZC8${9 zSo#=GHSaqIKiBD-U|Gi0O8^%dN+Cr~G2TFWnMJ=dNlO^-dH~`0I_(3YtOms?_1v^7 zqa!I51BYEDhD5G7y~g4fZ#|W3GzkxjjA!zr%B+Eh|!-o!7C9lhT8;rOvLhh5G!we z@|P8Hi@8?x5A2v@1;(myt>&2_MP`REsowk{Q+xX3HFkuMTt1tXul`*qjnoirVfJlzm0*oR*{#n#*nzo$U~(5 zQ_xYQXmE-6Z0PjanN!2|r?V=#95LNlpfibd29Fnw;P$uaTmBM>7+V-lXdmy6i}=pC zSap|T4jB#z|I2SznEcoom>)1Gc_Ty?q`4csnyU5H(Kx| z`0~Rfdx0B4x{&){YguawTut*j3!v8P2)_`?T?0SNtbRY>xzg;A1<0ur4;1x1o1$@n>900cyGmYhKZBpytlBBFzc#72-zlbRew zl9HO(O_N$cV$%en$uzGHjuY++_rA66`<`pfYNx}gs$IKw?ft8N`Fwl-^X>ilef)4( zv~FX9%sf2b_`AT%n3%Szg@%}5&c^$O+V^^2{4T(MN0K$!NqL3zRnGFII~sh9hhUPYxIx_f}o_L z{aoNh#o?edcnR)4>I`0>)YJVC3|?=j<xigu!p9hj;0`ckc{O^M? z`Ev>A(L@i1`lqiqf)Ck?EXh{;3?(mawz#~nHvLHRu@j6<%$gYvSPQiAZqm#Nqe(rE zBAyjRA0+ZPViP?e`~BciOr`z{U1F}^xuP=3GbaxUrt=Xc&ni%c21m04n+xrDq1o&C zG%EGnvbZDE^-uD+j$q64N<>!MoE(J(2PP=k4|G<`Cn16T(?grMdSXQ4Tc(G>qi2;C zL~Coxzi+2Y!88zEqy=qo#X!D|TiMJ5Swa%MJ=4+#vCsw9Es79)Ac;-f;RT1RIM*A9@%J1dw_L*OK5l6G8E8R?u+&($I39P#hW2Q1-CrO|Zdjg=V`c}N=Zo&xgY7ouLs}$3B9x{ppW@%g5AQjRaP?2~_Ig2)gill+X^&On# zSzXI^2+m?Oos2<#m>1w;y)J(y%8^}bF?FXW_CaY7!_nBA(}LU)+4hO4>l zkX4gtR%Msflnp*)^fz2~Q3y?EEHILZ$3oc^!q*3N5;pU(?pfo`_SW+EKU5$6@L{c6 zke<2tiIsqypVvx5ChY#z!g&2ViQlrm+5E{ys}ked3*&bQEENrYQ8+Qk2w5A?d(yF? zPHu_uOuQW2Lb&;B5}_f*jY3Kt^Xb}h*hEdY^vTtFmiI^RDM(M`5qw80jL(F=e7S5W zOq)bOT%^Cs3zl}ba97xDGw&AnzdV?Z;`=9Z| zT93JMfM>oi2|sHqJoKvX0%R0{^nH-Gq|>^ZQI~Yl)Ae(GaIyt|5T}I4YF4&-EaqIB z3A`RO5yZly7{hzZ!DF(CANEtMY;r%xe2ht8ua`RJtZCn5x%x1CprPV8N#CdkvKI#Z|C9)V z6%Fm96#L*BJ7QN3(Y+64k)qZmpfCk@m^1FWH?QO@xbydC49gIUZKRljaliFLCvynv zHqr(b?sFqPr+82yvTD+{J0aN*_H%hu;*+JlKNaSk~d*G)&ErPCQ6Nk1&&|3i4&#p)xyHgTVsRn zGu(i-KE_14iKi>;-rr#Ne^A?_nw!1laO+}x?(#eCyl1l2(czn zx$-xz^J4B1N;`gXZ0Pgwbj&JO2OXy~w-f0aOJ3XKYg=Axv*x8M9MT`i+fTwq%cF^{ z9X*kLCYSTOl#!+E$O)mZfOUO;@yls~#e)Z7DE<$^xm>ST!&D;@^kYU}Q+?fB%L3FT zu+WKX`5rqiI`tdN>^=4MRytXHwJT)Jo_VR_vhkLjMy|r;<8UTkYCqQw>pvzdA!1cY zBe7<1$E&8Msn^#jcb4_?vs@Z-W{+LH4z@NbAw4!d(LejHx^t)qD&++2OE4wawjdeB zqNB{)+leuT#OY^t#GGvfmnB~5psLcNl8s(sCC)^1j&}`)$61v6JU{DWo!WMm(-t=? zVPhej^X3Ib$s*Kj)%WG;C;B+MDf=r0L8(98;Dw*XP2dI=W9rY7+3alZEtCee&KId^ zjH)t9b?doEf7Y6H%U^P{vVBtDteF-XVrtIQv8f8LwVoaAf3}T@QrwCNiR1G-OWGwh zn+eDNxvg>pAy&kqQdp-p9N5GOsEK!srbxEi`)gcA)Xt#YldaFyi(_QnXRA zhrXY|`q!Rn!&+6-lrW1FZ zIEN2PJA&;{C7&i(O8Tg{eTEHAwKyqR@UftgIe+!!Jg=?mZKrlQ9S~81YjED9W2v#W zzpKAny6I;>(et**TxzRU{xvUe(6X``S5P?ELyIv@bja>D_xu)og^_Wdt2V>^*UtX2 zwwEjKGVF(67~$?ryG*JM#QLVGMkiZXAzQ;lcBvf_1}Z~p&8W*O@tMh26#b3slPk+g zt_~!sD{(nCNL8>p_jcq8LLlh>QmPn0Vf*myxvhJARw2Xnu1!vH?(18}XI$FDbM->B z_|F}Z<fAgv%_taF{8P$Hu#uDMPLv_8c`9hB&1iIg-AHNt>k0c)D4&({=vbt^_cl zNN%O~XG}du`{-W&$39-)P0GD^Lx-7%Niv{9_s%S;d3;!WNbu0r3Zw0uRM9D}8-WL< zsWTl@pVkDhABO#hm`-ojGq|OPK6Wt=h@-&SlONxgtSl~mjLehT-ZS+YvK_~Q%@!mU zhpGOzmQ*6M8&!UFnZf0Oedl`RZCuASY+q!ulw7x7(Q#*qS+)El?dGeN1Bxb9_s$)fj*2_6FN)j`?dRw;1jYfy#u2)vzsd=k+}!iQw{zendnVdThubWBusB-9B%% znHmadQ`os>iN70x9#3TZnJ2xj4gBp^Z-v_qCw};v@!HoEfxv^X zRD}^Hlq--)I@vI6M3d#9^7i3R$Sz14JUy@VM~M*|Z#YA68hRjIG`wtY5sw79+5Mb^ zPdKcTxlmHyaq|76#wNyAEBzZYeELRyFMm^f!s+q8GtpbkVbVQCxlVQa9)E}ZHRB<} zQ{=%3_z>>nw1Ut3W=#V)NXEgp?A}c1O8$+ESF0|)QWZmM4Pz^>+}H18F$zX~9mAoR zpOY~#&;cywc31Q z!8)*VZfpMc0Yz^4F6}aYjcs{l!fR|Up(K0iRUUi<|I{^TI&~$#B`3;=I&bs^?DZk5 zk#;{D)v|2G4;*)C_FKH>KMI_9@mj<}?trUML8@^DRd^^v$uq^?U9oqQ%eg^Hn3=oH z=jbz-4Su^CK^-39U4nMvbiDTr1mXOn zgY$WLBkxmBtjP2~O-!V?`q`%{?zZ?F5=D9@^U!n2-FWZ$?Z!pxLN7NdFnL9`J<&5~ za~(a>uKB5FKl2ho+dxGyFGx0zJ4vrrz58`nD1?FMK-l9nVJd!OrT?~eK8V57K|9GY zTpscRMGIP7*Yb}9QrZhv7F&Xq<`e%|C10?dZ26?Al2u%?DluAcwwS1slIuZ&JJ8(v z=!4RUnZ17xhFjS~Zm+<&sCC|TbigkT8yt@ zb3rmtR1S~4p^)dN*4h}|ayD`o_5IBRp@t%aGV*3WEAj;>5`Ce0{QJPxa zxY@90FF7{UZ32^i+R(%@A$*|GRLjo^iQ{FzE%m&m7<=n)u1V_fAFmJRfNdL~(;~NH zsPVPsZrjO=>4rk}ZBG{T8whN*L34dMM3vqxP7l+UnV$ID)LkCs#uLhv_C-yfRnhY& zL~`3XE!UaJrr|4yHUR{89+)faBkA3l~{LUD3N1yD6` zd>q^bH_5vtuyD2|RCES;NI9sFoey8~5+8bsqQ5tVIh>Ja@IRCekAC?_=>RH+?x63^ zWmH@yL1kakiS2K91Lb_nZ>APA1mi$qdQt+Z{*XH4PT&nEHVCZ&vZl$eqmdv%PqU^y z6#V`esT+RJFMx?XpyI>|swn$UFQN!Z`z!=Cfa7KBLMK5D);Y4%TpyvT(L%=tUH%ov zkCB1|L3WyS4kI0GYgt0~qY9kuU+NjhqBnEMw#l2r$?Z{E3Q8@*^cvX$KDT$P2 z-QRTwF$4P{Q{dVw)6P?k?#=uRaNz?w75?qnyF1rOKdZh$AD%WkE{NjYp7CrH(@GIQ zUxzvqK!zHtj;a;mx(D^@S~p8V<&S8t0J;0K(rr&U&L*zN~?ep z+~8{Wz&@xL;Tsk`ljwH3b5QYpkFKViG)To9Y>gbp7~Z-t^Q%riVhjEaE->4+nJCG5 z3qAwP(K_Gr|6A?7ucHI!_?cPHVR z6FsyNjeO*jOjchfqs*{$k}6JNUccf(i)^8fDoFViXS3kUv{%CpLFm)C2gIoX@(Y1x#$FerNw7RGOnYp{7s?O--?@B-IXDsa_0Y9ZdBgli@ZI$Ec ztQZGiidx7bgmu`Q+xJlmfgD9IQ5Sh|t^+)0u!g1wYS4Kl57~(c#{zzGvEv04}$c;pMMd3_`i(w|3}UV-^flGH|a0m zayKXs(to!+KtKA-LFf}$M^6A%2sIy4mfv0;#)gvI5;&+D;Azqk1LhVA#O({foVln@l3qEJnP$Pj1-buafn~wbEgSEj;i-> z7CrFwgkg>Gk3K|wTf6Os3XaaAv1QqbRH@O*%VdHCBXoJsnFg!Z>^WpM z!drUR?j1fL31}kxPn^gJaRQw9@s;e|9RBU4Cq6j|G^g26#DfxgdWlmjC+LG!5%%f+ zFPsP*_c?aCHP0nWz~zb*Zi4$lLe~S_2qnt?t29YwV97}~#7MAz z0*j%Y?$XCa_-~vDL649KJ^VLLq~{o!4_KL4q{uX5V!_7<>oi3JEKoMGita4Tt#vWF zzqVs)=mG`TAz{jOFH8YOd5m<)h##^LLzaKo6kAm^lXEam+G#W-M-K!AB+lHP>M%P5 z1O9k{s}uTNd_|o^2QfZIB1sRy_g4j_iFW6cAOJBcTm2G?I}A;;0Q)|&-Q4yfKy*ph za@ZZ1Xo9H--W1%!iS9Jh*K@cWglU3n_Tt`OG_eR;IfV@n1OccyLYG(uiS{-yb**C}WRw`maZ{7SbG2fnOx#pU?gYIqwu` zS>zk4AKU9sr`=yTQE(4&-U+2ws}s+(12n-YQ;1H0~@yp!-3X?(m`d*`$J>y~g@>L_ zg-=BYn`xHSxgRB|`}^DJJ%W;+0-MHayeqiC73_{cv)so_rsP(+$6QMROI5?haFy!- zON%ptCYko_5lF}HV7hPg zw?G8<^P2wr#T_0N)+xM1uc!ucp&JeP$;xQK{YMU|hxG*dBe>l&#@qYlmlv4C(T9O6gM++T0e4CwJR^fh;VmmiyH#4t8o{4gGdZ8~R z&-haTE04af!wkJ)ZOujZ3}Jb@*^hN4`JUMKi*@P#LikFs!4P~Mbz3RP0KA0k5EC*? zd?@thk3l8RD5HE)K%Ng~C21R_KqtYH(3)PESsopT6R>;4kd=+cIRp*r6I0XkM-T~1>3TPl7lF;~jV%(pancztPP1z+Ahmd@$0Ys945={`OEWf733 z_6}2Pv0U~+7UjocmQC`64P!#i6RyG^SpCrO>SC3%)+H|wI6TY|ZBGsx-g^0A_>l2j zG%pucAKfUW)J?Tn6!lT&>qxA8MU~SxYL4-I0!I`J9^xEheD@5aZz^szx|zS;9#M}= zAnH{|uo6+sDZK9z5|oh{0q0oyxITl*nO0VM~1Uho^_1S4CE&fv+kHa|NBHB2B0My+Cy2SnVwhu z-!-*2OHMmiL^+w~>C;u#*2uxrvOT^^@$&Femh#}zMBQAO(Tafm(@EjM+Z)`cTs-{= zsj0R2tqnzq`k4;TnfI227PKKzZ?)aW#jt%}QZb0-pZ7rVyQGSdhavqVgH@H^3758q z-YX09vPVQr&7h-%`=2`Lf1vpk5>b07u9RFkixn}Qm-KTcD!-nE-8v7WcLmLJCdZn?U+;KabbFMU7WiVX1JDSop?%9>aPF&C#l1wLQc)L^W3FdN> zM!|i1p8}=Sp9b)pN)ff&>#1Iv#dy6nPk4VtDCDuKHE^f@bz-`}4ijg^(9-9JD~mU_ zCfp`%1GoIEjdwj=*JtCbW-KkS)D$pz>?9o{3}o*D0^S7}_-rKTp9tMSq%NF5nO0DD zBEJ#2mX`5)0rA6D63;7x<2AjNWT`td*_#cTx;4talUYg$0GYw%TN`r$eZXI?nF8{p zc;)_d=2(oz6?~Y}BlVcd$#Vb$FnR7)Wfq30cBzM7WEB+2iA}8SYutQ;`+0^1W9PF1 zJAc`sba%6K7X%m-xL(D%O@t&kl!|$dZW-1(K7SZeL&r5^#8;qe9F{ii7w7_L;*t5l z084LIP~WE)+0t|lfKXw*cZByYxUOVPg*wxC88(Qe_=0#ZVWSu?4xykAL zfg!$08svpJ=EWpR>Om>kkBvP_2RI+|lin&0*ElWmIcCHPQ@-%5apt}f zx57Xog>=;C^2|uk`mc{GcekPdTuV-})Or{mJFM>hx4Y^U73#P4g6 z{w#W%UWC^;l(69m*A@y0$Z3<`pd~@x7!zdr7S0=Z>z(Jz)td~Ka-C}P%(l)ON19t* z6s4g}uluBGtORnRBJDP)Pq0bN2&@{9o7+a;1i|+P_^*kRE3n5IO?VUzAYZ)jFPz4j z3^r`-i%&6vV`Iq<#M|1Pop=lWj-+dY2-l^M7ARxHFI6^ihw_#`Ws@*ebQ{dc1${uw zH}||-rX3fn*uUPB6E!0odikoR9xiJ7`ljp+=RRAFdw5jR`^v7`1Qf5CL#?0nC7e$Z zUMM!^F2GX2{Y{Ud*u)7ZTw!Z)_of-idjm@tsN-NYn_K1(S|*2${Lx{#?~kC zT$GP4%uFcul3v!zy9gutn|z?77u4*(NdoIO8{CjvWuAAa4RR0w>QE@o5UqB zqN=q-Pd-n>(1uB7hf z^U-Av;KiCY;m8M2tWH& zuv^JGLuM`u4E%cEJ7J#gsASHTyAOcf5^v!0%FTV1wB;k3Zg9y3hSPu_*Thg`7&!9uX*hQc0tqYnrD9KMFa) zfM?J3DFDYun3_i()wxb=AYDZJ_e_x2y#ET17|OC6rfz?ql(7@e!V0V+@MM?DO=@j% zfQfu;SSy$Tvz+c%Y39SSW9e{zveBo0 zK*O9Z=;6^paDahvD#~(F)nFtGV1JG=>6Ng#6Z@mbwvO!_&gzL6x$1WJ`R^(<>BZ-= zo-z0(*1igbux%nEvL6KPr=7yys|#a9=tzF1E+F&)=y%2rZ?F50^g62@jpK{gF+3&d!fKb-vWasX05o{!9G(L>eU2NLPAeClD6Cml z;23;M1J1+ujV%GHXtWU^py-nw=5-eRC;LKVF-YN4fY?OAQS3 z#zMM!3Tuj=GX$_5_$80(rG_ibFkfS1KN!B|`E5Nje+!&hg-fBH05IP*rILapdAWgg zbSV8M3Vr@Y#nO1S<>aR|f7mp>NH-i|mOH2-J(^%pEo9|jwBxR#Hq_+Ic)CFeVN$w{C@W@MMh`#%rf5Q z5z}+DK`DPb*Utp}Bo0;dv!?I@1;&s1gND9wbxuBZlG)3pQq)|unx2FC&amvgZ`OK$ z))-PTVpcZN(@6$HR7z*pswAAD7KVR@S6iS7d2}74Z}(?O<5=jkyR?|19a;E*zR{Ii zBKtyQa-wje@?I4|Uoi3xO`{?Vf^BbGPI%2TiL*F20vtJcHi%S|^eJp_!>O}&3 z`%4~W9_rG|83Ob;vE8sx99fgLtUHcN3QGYqQWh7Vd8JiXOJx74b3vUCB=tBS{?7uU z{J3#a{1sb7?dYllq1>{}u*T6}&mT(S1Vu^U-IC=yWk}he@_3*Ddo|H-9u2pw=J2Fr z(+G{a!c>LTu$@5f|75TCO>F*w1X?zHenwCMqN9SKJxD4HX$gVR(AqK>B-P&8x#sj|VSh;+ z$qqm6>x?vz={DL21p;_u(b<3YKt>dg7Q}HLq&swdWnbK^X*q)Df3iy$K@Ku5BLe_8 z9Ih_2x6+eO2`@6@N{NFf_VSI7G)8JUw0n?rKg37} zy5;pZB)u1mJB->jpzj_4>;#bv3?~4R6ttC&$I!5dRW(j2Deb}z8UUUk%*GNyOeQ|9 zZ=4rt8AX?5cb5l^F`N)YK`8ip?*&Cp#N)Ysp{cL09%p-kr1e-Os25o?zZN*wvm;rX_OK*crNVlOO6={TBk&UH#Y&Xp01IZr(@NRU9-}%3Icme z`kJkp3-aRvxJKy4Ib7aZQTT`6(vcfN147#Q5b^USji8hZ#qvC-pf_C+Zn4KeoFXsw z)mRy}(^}jI=R`%X_?zWE@tNOm9yswa|2S#z@d+UziU|4v#Hjg@DVVJj5XAEaC9>N) z(+-4_kj2I4l=%s~jQ*^-5i1P`!a(bXLWKS6HCWodz-F5?MwfD6;}SB~ksqoD=NuOA z^@QGY&`l`C0wy02^FHkmMr+q2M zcAu6jXx-Aby-Bp5QsQ<%@ZOF~&!@@-aDXBJO@Ls;p^EU)>IT|ZVU!E#_s|<4Zi6k} z?a3{}wp+b-F3)qUn6MLWR$t_KwSfDFRvi1(}5Zhi)yG~$`g=pDHq&>MMgUT{k!Edhp zSiU)1RLp|Q6&wB9MY3zgjPmR>vt zZO{4aZxO=H6nTS=BMpE12`uI8zAU~&clC`eL{eYYe~IkT2da!x*xUxNIZV*Xe8CsE zohl;Gc3*(h-1*#V^K+MuArrlYJ#eDf&xqvHEXlYFC?~IB9^5LW7_VB_T1h-*h*w(t z=43haMh5sKF=LyE2m%djM;hvU2Hw`}IN;n)sw#@fZ*L@M)>p9jgdze@L>5Ik!OT{( zq`j#RpU`vdkD*viH~J;u+@X*VF!X20G2rU=q30pS`A1;XiVycW7RBOIIn=LV?(6+u z7VBAN5~%1#6nc2YX0JrzG5;`9*Mwb&IdL-p-ppEFSpRN;bGvxh`OVcFyu;5yG|co& z)fdZXc9P-D+`76mgQoP~SzH<|35>G%Rngps!YrK#PgJk0M0u|*n0?uN z3$#B9xs^6|r>8HX`tCxUnjli0l6D(LRbMPUSJ(9QxrFF|rdL|2uBZIn?b~JJT>#8h z_R9C)AMK;e%m|Sao7rpwTY*MPq`)=3m^-)$@v&z}f+`BPqi^Jk-w)b|D<8FPCak>8 zEm~$530crg%-qk%E^2lI4oKxu7~rM4wB^igYga!CHy_%)!`2qCL3s^yoG1GQqY?ei zUk?T1`RHoNy&#CBb zhG3Wel^OhK@o~PrY7l3KST77{P-BrrdoZ3{#et-H{tm9XSL##^OVou2vb-}Mh9gC5 z+3o!QOt-B0^9kk+lnE*3+|8}rl{d#HAE{H#22WAT87v4&m*NR-0s%zw0g||gwxM$b z``_)vTEHoJwju$_)lr=un`% z14)8=NJ$5C2nGs`wYtX-aM?Pq3>QWj_ZS~U1#Xz)@?uLzwiFZ<0xBeSP5l_8@4UJI zw6@4yfGD&r(WsQa&2;lpdl*aw8q@mzgRQyn_m7c%rODy%)dy;o5s+#rE zTBfkhBN3!+BOJ0~7kA;n zAsUAX8dh8hO`Z956x8x|JIMf-c{IO!m=}J|>Gg-e6h9&?zsQ`%xU)Qjc zv>AGO9L_6ZRxqNO8tt9g9p127irbhv8s4ogA#G+-^=oxhi-@3)!mU6Ojw*Oan(DWK z^_~+kdo1bVfq!eRyz1yZBJwbN%wn8Hw92$BvVF>*S=Sc*VP~(wSWqwN_0ZU#ucK38 z-BZf8-W3u$B<*KpH6DqryU zfe!vGx6SQrf#s4v7L3%XpzE3d#^RR4nqI3!&O3hDUBio7?^O0~QH`~8hz@@Zu;VJd z+bdaaAdgmT@1MTtDMB`LxO~J_QYZgNqm=mb0YdAeF&TUS8(4L4IYB&7_n7m&Vg`0m z-IBNA5*NpSWKxKboPWBcIc-}?-lrFDVbx2Esjo{ROUVXoTl>x42piSx;yg}argJ9t%XLiSlJB0^ zH%h4ww>5&T_@85>pVQTu3J0|u#+N^p&)6P(X9%=K8EnoCh8$N&cH6Gq-`&$B%zgMn z_#B8CfD)QFpxt`d?EW((k$c~=$B&-2`T!SlD!|dypjqH(_@a1Zjrjb|&P^vdC>-P+ z_WKYG%A8Gk)Nwj4#kT&Zm9}#PA4$b;H=L~DvMy%{>10sYW$mY_F6Ow~V5*bozfqm# zBzf%YA?cUJUXM{k2??P0!!88G^4{`le(QIDD{?aDT)g6Upm76YFJJ~gFp2Z0%mY2o z?|9g(w)1P&CMING`yZ`8tK4~xd6!d-GkJC1?VOWYFQbfGmY?T!2 z5+3e2_ToudT4NV$dN(}SK9H)W{5bk;(5r+LHTYq8&Ew;NGsa;Th4@`OR#mHR4#?;( zSR@JdUj+9j79(R$y9NHiLySXKH3+ASipT=XZ))m0^eCLu*7@EHqML=qq_$p!|5m%x zQ@Dt$;CYYUQTGEnYi z42%P}@JMd4L$4F_C|pGRD}um#%u~9bsI4rfTKn zLe%O}r@$wy1grwO^_|!keJvj&oOj zKXYym9MsP1Tm{5SK+_oeDK^3p4jQJM<^Yj?tS#o^_!+7~N5k2HEa@Oy9X0p8LH=_S zNA3|JLj;cD(1cu4KmwYZgwz=Jj(s<~B3V`Y7#-u4{3Glw=N&a~n!l!?)23_Z-FC++ zbIdVxT)TIHa_MW=>Pjcvk`oE8rNvV68bJY>md@aow;+4!S!Kruw2_$BG3T72{N*gA zHN}F9@yf6`nQg=n_Nx7jQnRbKvg$97`EzZ@e#BvG+Yuvxc#%lZOH? z{^bQl#S}8@gV9{U3}7Ku?cEVu$KMs(S2FglwaD9HKo}l7DZ>nV_uxDxO$@dtn@1<< zmt8`})k9_&a8Fis5@sw%yC)(WKk_T4op=<^&hB=NC3D*p(beNhMF9&ERCGy24_3(| zcRutf)!>eeW%K+a2mFm}HpX5~=Z24#_>p=Cd?&agu)els-nQwJxwcap`xj8(OUPhI z@}O}IH7_|SIbBK`xoM$KA-e$C{V1>V@2&b1VH)1W7dw>$=B)cEeWUr~t&c1h9$3A{ zSiDu$&tCoVAIt$Rk43YUOjYy264OSBOrSt64G%})e#<_z$9pVtCq%g>OP7Jp5lFl* zE}t=zaQPu1H&tAy?Q4K55^z5=>ND+T2_SdP0v8|BPjGdCQv>}?1xf%`XQ?twLT+sC z3AM=4@(O5CoAJbRTi2D;MoqUY7xqEtI#m-|8!@9|3;y}E?&w;qTJ-mhzK2Lu=xTei zk$$?MQCV}#J97$*-k`L{h*@PE><6(8vEk;nuxL9v>&LdNGUUF6i4yxQQJE^y!8k)E zj;lwkD451Ngon@bqBd1+#0KzerANa_jt#>XyFI-fKzl~tR|TY=&>~T+R{84;&pfh0mB;4SOaNDjI64Di0;ONZ~ z6eLejV)xv{FW$Fmw;Rpf^^=vqnsxk9_;lvfNS1S>QSY6T$Lz|7w$_QbQ?fDVZy5`7 zOlyMt@y2y5pYhPn1ais&lA>{{@t3EsvA1zoZ#c<9;Rt^D4M$jyL#!($9eW)^&t*C` z21j~$e7^3fr^`0*Vr>U(=uu>gt4d<>Buy`m=qNM6RngAhk>h}Bh0^3?i*dlkTi{F7Z_BQ+i^TE3G?$&XiiCw<-WdO*1p7>-b%~m3 zpJBL{eKwY&HL3R&#@YuDG{{v0xn@oXt(YW|piYi>gdt@uYpFKRO|-qSK~mM7gquz> z(B-S3P3yOL1BunYgEoh+wDh;(f=31ZQSPSgd?UE;pmi|pGkV8Lb;%(4Yd>_OYbq+0y2IvUCNliHUZGIPCxsF?69z@2qJPBcUuXmLeHAjTSS zaXYJbxQLCmGXTuW>AT;iGZ!(HiC6^iC=_%w`NiQZpjg1LlB}iS3Q|0Vh)?}C?U#m9rc1FFMFi-?&4^_m_I3zN2$Iv#u%XlfZ*{}eNelA&*R zs<&HPTnvQ|Bq2Sn|2()rX2g#u$ofB^{|YzCm4j>PIU)PBZh~%E+BUP(*Vm4~68!$J z>A%L@2z=pn0iCed{MTKpg_|gw%-MT1yIwpVDdQs%GV|mwzK$-Hy2p%=Q?^q#yTgALkRmr$F!2)$$kCoZ;{$~)-= zzz^>EuM%q8=Ka4GYzQ3_UEg-WuqrPV69$#;H%iY@7Rv9h7M7xvw}@^^{;RcS{*&F% z0gH?dRt-h6AxX zn9BLhnYqzWzPOzY&E4YL^uhIDalF(*Rj;UFc9#oFJBIxRP1PoL`}TOg@I!`rvUN4ho<%1nOL1;O zT0o9_4NLYJs)W~X+WG9?qC<-&ya_}VCVae5NB4O1Q=iQFpaA8-`Qu?QJU%J^b~g8~ zVYpBKnC~>s&v&QQy>k428tLWMlrfQ7Hzseo^dwI-D&j;@&AnTzfi2$qTK97u&Pe16 zYL&nFZg;U}BV!Gl=sT&2(XXPyrpvoQ#DjL{`^;0*BL{&kN=O^^J#c(&;lg`!s_p6) zan~}l3eShy!V=)^=5b7D&=QHjS_|8PcRjIG-x@_0rFA}x`R=BV8rDsKv^RrulCJ{4 zf3cftu9!cQA+2bZdlfxi7s?xxmok7Y3pf{{xY}c+shk+cGT48#t!4cZ!N_TkScInzjm4vmyk_g+#z~VUa{#BMby8WBq(nU?T7jeg5 zZ068akuixKlXY_CalJELm5H+f@!m}b!Ybr_x5Ne7^S-`_=F8f-FxQZCcg6-UGJh}g zLGOK@iB|J+Ux)I7+5VmN<{h^W4zoA#T+|KXBq^g~@TPuENCYQ&g)PhoE-@n-dd-R? zS{h=hsH~e2qbjS(YPp63sLP`MWcevK8IHV^%ovi;c@9w-oTt>9#q6$mUeI`DG)~^T z&Mwc$sDv)p1=4G_PZxSJktI4@W`L*21o>nJ$P9L^a5pE)**qtMHNWDzY*rkT*2&u^4OM7xE&*LgBG{++kd zLB^{w$iyv*NL|hJ-6`b>9J@v6&k8IV^uD0rS|3OZAid=~FO2DsCJm9-1P7_iu9XWu zJxZ|}t9>Pd2j@f|GX0`c`m$zDY9W5Nvt}wVU05maflK=#a~M^-B)nj}l~wr2>#j^@ zPl}Hs-|ja%cFOFbFZwl`Xu{6g?%f~;$KksQ+)y=TJY8^J^b3^MlgL=A}G13!bUmA>hbTE>)InL zxN&{DlU`09*|6vQ0rQJVId;36y6Me}7vBz)Mw_foY#cAEIOkojQQAJTk*LEq;nkPp zisH>nzunn0+b3ST#6^r-2$Q?ncOvhSk&wC16E^{`Cm`=ePeo1zxQ@J?oqyohc-O_B zNzizs)UDjpfv4VmHn(!Y{G8jYYjT#3J`*Zn?Bl9rqc{8}alBv0E^BX&eyof%ITRCC zqEDnncFI(CYlKHRc3~mU9By5nqOS2RCg>&X;>jpvFTLV>Kydu~EuY>ZIf^`H)}*Mz zbx_1bxf2*Pny ziGec6to64qe3jk`DkY~7tj%382_LEa{lQ{=XC0I)ywn!+bLM;PZREYwKd86LU4*UXKI`nM4hi})@yX7j@w3mzJYuxPpVmU9Vdwt7OUW0RD+Xly-8O_rx zvzBl3%(32-bJ9-FEr}rINNkV5eP6hv@qIITBinO!ETVl|v-sL-K|a2%#2sbb1m1cY z(_vkmh;J`Qy-C*VIiAC&61kpJ32VYFMBlkgA^OVVya9g5OV>5-fdSLI0kD)_v`}OM ze6;QQ6lA=(e1!5Y&6HL3F3+MnLN%4$G6SBI1r?hM(mQSsg8g=yFbP*XO6VkFLuiXC z&eb?B8WB@IMe60zC63FHz*;zkuA++sCZ?^{OUKEGEQsq?o(*+;8Gd%)%X%5l`OdlG z8M*XzUY#B0)%mT=f#$TWteN4P7Q08BRo<-yeSMPc2roZRc`Mgidp}5}Gw79%pHi(M z{tFr1lVYd1z{%)33(tNjtIEv2lowFX5E0QE)#_d8p_5QpaXAG|vcNX#8D^KB>(3DS z109e)^)-}qv0s5l-gh%|YO=ZTt@2F$)cGhpl-n}0*})>rYgZO#3KPlQ0pG@_L}y(*N!wR% zD#Ig6S+t5Hr5Y~A{=X*;Oq2!mi;z72k20=3EXlj?TXtBMy0_-CJnh-lG;>+$!~>xn z*TK{f@hn#S<{*hh+fp7mnSxS+sLA~aydnZ3_^~c&CICpn zSOuVau`D`C@)=~z7Nn0J`5%$4UfByEj!P{ijB_~GYdrSff?*{K~v1KhhK49V&&g) z>en9)w?W5RMglKL66*_c_A>d3T?z=Ik|mU*C76prn!d1LT{jNtnHc2R{di^L;Izoh z`eAmCsF6=Wv)V@8)1JR?n4nr*ba?XM>|f|I|$5aDp>V$fsbYz)N_|JY5Iy(MNF z=_R^cU6ShWHDbO<;rt5?+*Q1HB*3t(Oi`ZYz;VCYS2y^V$cOVEiq{RK+l!bbbKp1i z-4|R7w0~Ui%ka|gNR{Rz6Vjuv2;&iYC(92Zp&wu|xqV?IL=usby!nP|aUbaApzpi} zji+H2OrC`?oG9*j0lmZS=vbXQ#q6UhqUBTj()m@803udSaZj`|)A#ni{_ep}>9-|p zlhp7QT9&|cPoJe6Tt(ETOgv5N;U#lE4%4LCzo&493VmK#zV1yL-N^{Ri^p^>XUy^+ zmP6vkKeG8v!f6^Rt$vL;D+u8&Tcq@~=u10BRF!vfCg0twvUYxJkOB4McU#*HVStB% zfY?-`Cw_50U}pybv(Zebh+p+>L^Ss+MM0@>Y)zrgPHIKp4JqonuIPTWGZ~F)xEk>Z zQl=Bs))5m~s~{WS4GE>jzqv68sd(vBJijVQhEG1lxG?Ru_=+kU zGKP$IKw-dU(ON$d;jEVivv-bSUB~P2rFpIxrF_@$1wYX(QM)W!XCyogB+H-cZ{zk4 zetHFxui}qQz9XkjL-^vz*uju*t9uU_iLZNJE#{$Hy3p*T2Gf9DjM`G+GNSiG#mgh{ z4t_&Y1}k|={ihE;d`fLjfGvf(0-+=w;2SVQzJP9QmvCf?nNuX$*OQUZKX1E-BTun; zzckUiU~>%)bAN62nCC52;i|e5ryD7?MN$V9?{VY}u+M~xAd|@4$J2Ok+~x?x2BXo^ zs|SL4Gzn?Glz`<-6*|-GdH1sWpL_j(h{Eqmn)869U~;5c?l`SL3`RdQu>2UhPt@EY zT^gAD@J_}1P!R+3L19IO3n3XJcKrSbC2fbz_eOfg@~r-;%Hy!+*7LBcKq3x5-nmuE zcoNmY9QA=og)~#Q5+zRrQ4X{XG5Ko6tZez-}XJCl4eb7 z-J*cXE=prtAG0Y1rfEFKcZT%)9Or;+aRJ$K@CiK`ap2sEx`n>awGuolP%HoIx;m|+ zp)G_2Y*}i!q+Z!|(r-}&qLI7F=(xdN-a8!jHiuq>s)d#bt|Xpvr`K}2$_SEjgpWyG zjv=!>W~p~hKN0bqy2Y}C1uVLRlT35Bg&n9Le?gP9JXWMJYDFKEw-6=ElMTbl0~<~$ zKmDEIak$E>*iXzq-HF7H`TbnZl!x>xG`C^lDNt7VDZvlBk3bE@5ujvkO3qc1K`-~ zi-Qa67{ViMqqJ>|^?Bumh|eP!5l*HvBqTV;!O;u5+X{5F2KG>u4nMjz*rNooga@Ru zo7or8QG*v40nq{dvB$U-tcyq?Tp9U0H%@Kt{|H{isb$-ED;N;&5m@LT%0L6|p$ec1 z)_x|KO_ttvod`4Evi+Ogrirx8FUBlzTHQiu! ziIRzyz>433XQn5KKMRheT(&?$@@e)LtIhh|7+JdLlV-uuA68|wyDCrTVybpFi&PWS z&mg#C$#UK8imXLXAIAh1V8<-`RbLVpto@t6kB9?53KvZ*l4~0c=QHEV;i)NHmH|79 zhUIOzuWYirBmRiIrnGOB_npvvpDOf{Bm@>yx<<+;LW!_?uj0?B<@q(0H6RUAFyGyTLytOXWX+bB#_7sz63x(3{xueEO_ zCTAkSRdLI*PGWYz=wH7vQ2xnAV11=mZQ3_7eJL^F6lW5l7uzuCi*2|Pzc@7wlTF>kk%kgU#|`pyYm3IdTLA)p3hA~1T4oF3 z|2wL9DGJcJ9jNNZHhgs{Xg=6-xOV*z>4M!$MO_zU4U3{pB_mD97{5b?lIXS6cIm4Z zotMEi-G(J+P8{_ju^0bSr63yWY>Y+u@)wX0Go!|#kxXJ9+e=*Jq!n$FAX*#_u>Cwn ztIWg2G;UI~K&^9Fo(La=cMv`3&oV+DZlKEzs;n&QG6p;!xs3Kh|z~EHNURYf9&r!Fdl@dD#KM;!ZpWdcG zxgM{1$d^t((t6=O-*BDgrlru14x)Wj;l!_wiq5Us1D>!Jlr~kiAXsK{X(WQ+Q13*^ ze44?Oz_iq?1&EvV0(gnw>I)x_vIBD|^m`6}8V7v@YD)r_gUN!UhjZQx*WkQxt zW6$6wA@z(*)+YgHdSe;K2o~joR+?~V&hd&BlQB(Wz4`$`;;PP)EQCis@ra7Uj#Dwl zcjJ4COnnymxu$ts-R>LTtEeR7NbrAUFPaR7Nt(PAMN#08c?rCfL-55S$s3I}vu^`m zzddb>zru5$G1q#Q4DQ&~bInF2E5?Pj<9K-l$8uCOd1LAj3CaCh+n6FVc!Q0>j~a<< zrjSWmeOGSjzAf*`W0y&<%Il}6{-CXNj`7T9qb;&s%R|qpTBoS2(IrIX( zt?|t(pFnU&^exWN?Av)(lQLj&*5Kkci#;^KOa2w_Ho2C=@AxIE9BuJrmZGMih3X1( zfAKZFjarG8UqEiQcw$?*&U4f1s3m%abNm~Mv~yBycwlSnAC8K9TlE}VdIJJv{Ol2& zR=L?0@a+Q#4`+L}Ld8aBqv93gJJ7*Snoe~-c)O>SJs0h0CfI1~;2D(ALa)+a&{&|` zNU^O8c=abtzCTr4OY^V_p8D#`s)M)V*KJ<{pz-I{gHL!IECJ-BhKat%s;0n4vZ|gw zdaw?lYY|mTL{~Tzd|gZH%t`kXWyeBp(a>nqf;^h0fm?dr#oNdpRxsAU`lDrcM%*^B zDXmF-0`M%27E`iaN|`lDg0Y&pbLme!9PjI^EuSskdwqWU#K?7n+>oPtH;Y7pk%^Oc zPJ9a|7Z&XKA@i5?46CS;H-F)dUGA&IX0w2Wu`~Y@df#p!M8>EKX)q28KR9XD^Oh0R z_*06@(q)DVwIk-RkFMDa((Z%D@|a6E9y`=6OLfUBaBm2??_zO~ur;gHdnOq0q5HG> zX7bv`)cmul(pO1J6QFD<;2b?cBH8Yk{nNjK5uP_Sy9x^S47SbegEU`=!rZyn0?9V2 ziJEzHk&2LoX0Gq4|9IH#>RGop6~7NN8}4t*IQY~6{{Ha({h9$6Uvo@Eik~pncMnRh zF$p(xJb7tx=Nx4$h~BuxwP-90_8`al{Un!%i)wfMTi2VT2_~cPqt_&1GK-v0^TpiV TiIZC*m8=(vwyLTepQ8T-8xIDG diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b24718f..c2519452 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,17 +5,18 @@ minSdk = "24" targetSdk = "34" kotlin = "2.0.0" -androidGradlePlugin = "8.5.0" +kotlinx-immutable-collections = "0.3.7" +androidGradlePlugin = "8.5.2" ksp = "2.0.0-1.0.22" hilt = "2.49" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" androidx-core = "1.13.1" -androidx-collection = "1.4.0" -androidx-fragment = "1.8.1" -androidx-activity = "1.9.0" +androidx-collection = "1.4.3" +androidx-fragment = "1.8.2" +androidx-activity = "1.9.1" androidx-hilt-navigation-fragment = "1.2.0" -androidx-lifecycle = "2.8.3" +androidx-lifecycle = "2.8.4" androidx-navigation = "2.7.7" androidx-preference = "1.2.1" androidx-recyclerview = "1.3.2" @@ -27,12 +28,13 @@ shizuku = "13.1.4" viewpump = "2.1.1" material = "1.12.0" glide = "4.16.0" +coil = "2.7.0" gson = "2.10.1" flow-preferences = "1.9.1" timber = "5.0.1" libsu = "6.0.0" junit = "4.13.2" -robolectric = "4.12.2" +robolectric = "4.13" roborazzi = "1.21.0" androidx-test-core = "1.6.1" androidx-test-ext-junit = "1.2.1" @@ -40,6 +42,7 @@ androidx-test-espresso = "3.6.1" [libraries] +kotlinx-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-immutable-collections" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } @@ -76,6 +79,9 @@ hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } +coil = { module = "io.coil-kt:coil", version.ref = "coil" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +coil-test = { module = "io.coil-kt:coil-test", version.ref = "coil" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } flow-preferences = { module = "com.fredporciuncula:flow-preferences", version.ref = "flow-preferences" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } diff --git a/settings.gradle.kts b/settings.gradle.kts index e60413c4..494ea520 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,31 +15,21 @@ dependencyResolutionManagement { } } -include(":app") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -include(":data") -include(":strings") +include( + ":app", + ":data", + ":strings", +) -include(":core:core-arch") -include(":core:core-context") -include(":core:core-database") -include(":core:core-datetime") -include(":core:core-intents") -include(":core:core-io") -include(":core:core-navigation") -include(":core:core-preferences") -include(":core:core-terminals") -include(":core:core-tests") -include(":core:core-ui") -include(":core:core-ui-compose") +private val modulesDirectories = setOf("core", "feature") +requireNotNull(rootDir.listFiles()).filter { file -> + file.isDirectory && file.name in modulesDirectories +}.forEach { file -> + val modules = requireNotNull(file.listFiles()) -include(":feature:feature-crashes") -include(":feature:feature-crashes-core") -include(":feature:feature-filters") -include(":feature:feature-filters-core") -include(":feature:feature-logging") -include(":feature:feature-logging-core") -include(":feature:feature-recordings") -include(":feature:feature-recordings-core") -include(":feature:feature-settings") -include(":feature:feature-setup") + modules.filter(File::isDirectory).forEach { moduleFile -> + include(":${file.name}:${moduleFile.name}") + } +} diff --git a/strings/src/main/res/values-ru/strings.xml b/strings/src/main/res/values-ru/strings.xml index d6ec9080..7ca76ee6 100644 --- a/strings/src/main/res/values-ru/strings.xml +++ b/strings/src/main/res/values-ru/strings.xml @@ -153,4 +153,8 @@ По количеству сбоев Использовать разные каналы уведомлений для уведомлений о сбоях Открывать вкладку сбоев при запуске + Перенос строк в деталях лога + Черный список + Добавить в черный список + Убрать из черного списка diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 860244b1..9ff9834a 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -162,4 +162,10 @@ By crashes count Use separate notifications channels for crashes notifications Open crashes page on startup + Wrap log lines in details + Blacklist + Add to blacklist + Remove from blacklist + Are you sure want to add this app to blacklist? LogFox does not observe crashes for blacklisted apps + Monet