From 448e0e139a8cac2a87dc05645642327ad544724f Mon Sep 17 00:00:00 2001 From: Alex Kucherenko Date: Tue, 13 Oct 2026 15:43:20 +0300 Subject: [PATCH 1/3] Release 1.4.0 --- .idea/copyright/profiles_settings.xml | 2 +- app/build.gradle.kts | 52 +- .../nextcloud/client/di/VariantModuleTest.kt | 57 +- .../brandIonos/assets/branding/config.json | 23 + .../res/drawable-night/nextcloud_logo.xml | 84 +++ .../res/drawable/ic_launcher_foreground.xml | 41 ++ .../res/drawable/ic_launcher_monochrome.xml | 21 + .../brandIonos/res/drawable/ionos_logo.xml | 82 +++ app/src/brandIonos/res/drawable/logo.xml | 82 +++ .../res/drawable/nextcloud_logo.xml | 84 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 13 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 14 + .../res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1132 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2706 bytes .../res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 878 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1858 bytes .../res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1680 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3766 bytes .../res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2364 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5874 bytes .../res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3000 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7996 bytes .../brandIonos/res/values-de/ionos-setup.xml | 14 + .../res/values-en-rCA/ionos-setup.xml | 15 + .../res/values-en-rUS/ionos-setup.xml | 15 + .../res/values-es-rMX/ionos-setup.xml | 15 + .../res/values-es-rUS/ionos-setup.xml | 15 + .../brandIonos/res/values-es/ionos-setup.xml | 14 + .../res/values-fr-rCA/ionos-setup.xml | 15 + .../brandIonos/res/values-fr/ionos-setup.xml | 14 + .../brandIonos/res/values-it/ionos-setup.xml | 14 + .../res/values-night/ionos-colors.xml | 117 ++++ .../brandIonos/res/values/ionos-colors.xml | 132 +++++ app/src/brandIonos/res/values/ionos-setup.xml | 68 +++ .../brandStrato/assets/branding/config.json | 20 + .../res/color/radio_button_color.xml | 11 + .../res/drawable/ic_launcher_foreground.xml | 32 ++ .../res/drawable/ic_launcher_monochrome.xml | 31 + .../brandStrato/res/drawable/ionos_logo.xml | 67 +++ app/src/brandStrato/res/drawable/logo.xml | 67 +++ .../res/drawable/nextcloud_logo.xml | 67 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 13 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 13 + .../res/values-night/strato-colors.xml | 139 +++++ .../res/values/strato-colors-palette.xml | 45 ++ .../brandStrato/res/values/strato-colors.xml | 149 +++++ .../brandStrato/res/values/strato-setup.xml | 78 +++ .../analytics/FirebaseAnalyticsManager.kt | 12 + app/src/gplay/AndroidManifest.xml | 47 +- app/src/gplay/ic_launcher-playstore.png | Bin 0 -> 14102 bytes .../analytics/FirebaseAnalyticsManager.kt | 23 + .../progress_bar_login_indeterminate.xml | 16 + .../color-night/drawer_item_shape_color.xml | 11 + .../res/color/drawer_item_shape_color.xml | 11 + app/src/gplay/res/drawable-night/favorite.xml | 18 + .../gplay/res/drawable-night/file_image.xml | 25 + .../gplay/res/drawable-night/file_movie.xml | 21 + .../gplay/res/drawable-night/file_sound.xml | 31 + .../res/drawable-night/shared_via_link.xml | 19 + .../res/drawable-night/shared_via_users.xml | 26 + .../res/drawable/account_circle_white.xml | 19 + .../gplay/res/drawable/add_to_home_screen.xml | 16 + app/src/gplay/res/drawable/all_files.xml | 16 + .../res/drawable/bottom_sheet_drag_handle.xml | 13 + .../gplay/res/drawable/dialog_background.xml | 18 + .../res/drawable/dialog_item_background.xml | 16 + app/src/gplay/res/drawable/e2e_border.xml | 14 + app/src/gplay/res/drawable/exo_divider.xml | 16 + app/src/gplay/res/drawable/exo_ic_check.xml | 16 + .../exo_styled_controls_audiotrack.xml | 16 + .../drawable/exo_styled_controls_pause.xml | 16 + .../res/drawable/exo_styled_controls_play.xml | 16 + .../drawable/exo_styled_controls_settings.xml | 16 + ...exo_styled_controls_simple_fastforward.xml | 16 + .../exo_styled_controls_simple_rewind.xml | 16 + .../drawable/exo_styled_controls_speed.xml | 16 + app/src/gplay/res/drawable/exo_thumb.xml | 12 + app/src/gplay/res/drawable/favorite.xml | 20 + app/src/gplay/res/drawable/file.xml | 16 + app/src/gplay/res/drawable/file_calendar.xml | 16 + .../file_details_search_background.xml | 13 + app/src/gplay/res/drawable/file_image.xml | 25 + app/src/gplay/res/drawable/file_movie.xml | 16 + app/src/gplay/res/drawable/file_multiple.xml | 16 + app/src/gplay/res/drawable/file_sound.xml | 31 + .../filelist_action_icon_background.xml | 16 + app/src/gplay/res/drawable/folder.xml | 16 + .../gplay/res/drawable/folder_star_32dp.xml | 16 + .../drawable/grid_mode_item_background.xml | 22 + .../res/drawable/grid_mode_item_outline.xml | 12 + .../grid_mode_more_button_background.xml | 21 + .../grid_mode_selected_item_background.xml | 22 + .../res/drawable/ic_action_create_dir.xml | 16 + .../gplay/res/drawable/ic_action_upload.xml | 16 + app/src/gplay/res/drawable/ic_activity.xml | 21 + .../res/drawable/ic_alphabetical_asc.xml | 16 + .../res/drawable/ic_alphabetical_desc.xml | 16 + app/src/gplay/res/drawable/ic_arrow_back.xml | 16 + app/src/gplay/res/drawable/ic_assistant.xml | 16 + app/src/gplay/res/drawable/ic_camera.xml | 16 + .../drawable/ic_checkbox_blank_outline.xml | 16 + .../gplay/res/drawable/ic_checkbox_marked.xml | 27 + .../gplay/res/drawable/ic_close_search.xml | 16 + app/src/gplay/res/drawable/ic_cloud.xml | 16 + .../gplay/res/drawable/ic_cloud_download.xml | 16 + app/src/gplay/res/drawable/ic_cloud_sync.xml | 15 + .../gplay/res/drawable/ic_cloud_sync_off.xml | 16 + .../gplay/res/drawable/ic_cloud_sync_on.xml | 16 + .../gplay/res/drawable/ic_cloud_upload.xml | 18 + .../gplay/res/drawable/ic_contact_book.xml | 16 + .../gplay/res/drawable/ic_content_copy.xml | 16 + app/src/gplay/res/drawable/ic_delete.xml | 16 + .../gplay/res/drawable/ic_dots_vertical.xml | 16 + app/src/gplay/res/drawable/ic_edit.xml | 16 + app/src/gplay/res/drawable/ic_email.xml | 16 + app/src/gplay/res/drawable/ic_export.xml | 16 + .../drawable/ic_file_action_cancel_sync.xml | 16 + .../res/drawable/ic_file_action_edit.xml | 16 + .../res/drawable/ic_file_action_lock_file.xml | 16 + .../drawable/ic_file_action_move_or_copy.xml | 16 + .../ic_file_action_open_file_with.xml | 16 + .../drawable/ic_file_action_set_encrypted.xml | 16 + .../drawable/ic_file_action_stream_media.xml | 16 + .../res/drawable/ic_file_action_sync_file.xml | 16 + .../drawable/ic_file_action_unlock_file.xml | 16 + .../ic_file_action_unset_encrypted.xml | 16 + .../ic_folder_overlay_account_group.xml | 24 + .../drawable/ic_folder_overlay_external.xml | 22 + .../res/drawable/ic_folder_overlay_key.xml | 22 + .../res/drawable/ic_folder_overlay_link.xml | 28 + .../res/drawable/ic_folder_overlay_lock.xml | 22 + .../res/drawable/ic_folder_overlay_share.xml | 24 + .../res/drawable/ic_folder_overlay_upload.xml | 24 + .../drawable/ic_gallery_set_media_folder.xml | 16 + .../res/drawable/ic_gallery_show_photos.xml | 16 + .../res/drawable/ic_gallery_show_videos.xml | 16 + .../gplay/res/drawable/ic_gallery_tick.xml | 16 + .../gplay/res/drawable/ic_grid_mode_more.xml | 16 + app/src/gplay/res/drawable/ic_history.xml | 16 + app/src/gplay/res/drawable/ic_import.xml | 16 + .../res/drawable/ic_information_outline.xml | 16 + app/src/gplay/res/drawable/ic_link.xml | 16 + .../drawable/ic_list_empty_create_folder.xml | 16 + .../res/drawable/ic_list_empty_error.xml | 16 + .../res/drawable/ic_list_empty_folder.xml | 16 + .../res/drawable/ic_list_empty_recent.xml | 16 + .../res/drawable/ic_list_empty_shared.xml | 16 + app/src/gplay/res/drawable/ic_menu.xml | 16 + .../res/drawable/ic_modification_asc.xml | 20 + .../res/drawable/ic_modification_desc.xml | 20 + app/src/gplay/res/drawable/ic_pause.xml | 11 + app/src/gplay/res/drawable/ic_play.xml | 11 + app/src/gplay/res/drawable/ic_plus.xml | 16 + .../ic_progress_bar_login_indeterminate.xml | 21 + app/src/gplay/res/drawable/ic_rename.xml | 16 + .../gplay/res/drawable/ic_scan_document.xml | 16 + app/src/gplay/res/drawable/ic_search.xml | 16 + app/src/gplay/res/drawable/ic_search_grey.xml | 16 + app/src/gplay/res/drawable/ic_select_all.xml | 16 + app/src/gplay/res/drawable/ic_select_none.xml | 16 + app/src/gplay/res/drawable/ic_share.xml | 16 + .../gplay/res/drawable/ic_share_settings.xml | 16 + app/src/gplay/res/drawable/ic_size_asc.xml | 16 + app/src/gplay/res/drawable/ic_size_desc.xml | 16 + app/src/gplay/res/drawable/ic_star.xml | 16 + .../gplay/res/drawable/ic_star_outline.xml | 16 + app/src/gplay/res/drawable/ic_synced.xml | 21 + app/src/gplay/res/drawable/ic_tick.xml | 16 + app/src/gplay/res/drawable/ic_unshare.xml | 16 + app/src/gplay/res/drawable/ic_unshared.xml | 16 + app/src/gplay/res/drawable/ic_user.xml | 16 + app/src/gplay/res/drawable/ic_view_list.xml | 16 + app/src/gplay/res/drawable/ic_view_module.xml | 16 + app/src/gplay/res/drawable/ic_wallpaper.xml | 16 + app/src/gplay/res/drawable/image_32dp.xml | 16 + app/src/gplay/res/drawable/nav_favorites.xml | 16 + .../gplay/res/drawable/nav_notifications.xml | 16 + app/src/gplay/res/drawable/nav_on_device.xml | 16 + app/src/gplay/res/drawable/nav_photos.xml | 16 + app/src/gplay/res/drawable/nav_settings.xml | 16 + app/src/gplay/res/drawable/nav_shared.xml | 16 + app/src/gplay/res/drawable/nav_trashbin.xml | 16 + .../gplay/res/drawable/notification_icon.xml | 15 + .../gplay/res/drawable/notifications_icon.xml | 16 + .../gplay/res/drawable/player_ic_audio.xml | 31 + .../gplay/res/drawable/player_ic_pause.xml | 19 + app/src/gplay/res/drawable/player_ic_play.xml | 19 + .../gplay/res/drawable/player_ic_repeat.xml | 16 + .../gplay/res/drawable/player_ic_shuffle.xml | 16 + .../res/drawable/player_ic_skip_next.xml | 16 + .../res/drawable/player_ic_skip_previous.xml | 16 + .../gplay/res/drawable/player_ic_video.xml | 22 + .../progress_bar_login_indeterminate.xml | 14 + .../gplay/res/drawable/shared_via_link.xml | 19 + .../gplay/res/drawable/shared_via_users.xml | 26 + app/src/gplay/res/drawable/uploads.xml | 16 + app/src/gplay/res/drawable/video_32dp.xml | 16 + app/src/gplay/res/drawable/video_white.xml | 16 + app/src/gplay/res/layout/account_setup.xml | 140 +++++ .../res/layout/account_setup_webview.xml | 48 ++ .../res/layout/activity_preview_media.xml | 81 +++ app/src/gplay/res/layout/activity_splash.xml | 60 ++ app/src/gplay/res/layout/backup_fragment.xml | 134 +++++ app/src/gplay/res/layout/deep_link_login.xml | 82 +++ app/src/gplay/res/layout/drawer.xml | 92 +++ app/src/gplay/res/layout/empty_list_media.xml | 72 +++ .../res/layout/exo_player_control_view.xml | 217 +++++++ app/src/gplay/res/layout/exo_player_view.xml | 91 +++ .../res/layout/file_actions_bottom_sheet.xml | 94 +++ .../layout/file_actions_bottom_sheet_item.xml | 63 ++ .../res/layout/file_details_fragment.xml | 236 ++++++++ .../file_details_share_link_share_item.xml | 102 ++++ .../layout/file_details_sharing_fragment.xml | 243 ++++++++ ...ils_sharing_menu_bottom_sheet_fragment.xml | 149 +++++ .../layout/fragment_gallery_bottom_sheet.xml | 134 +++++ app/src/gplay/res/layout/grid_item.xml | 265 +++++++++ app/src/gplay/res/layout/info_box.xml | 36 ++ .../layout/item_quick_share_permissions.xml | 24 + app/src/gplay/res/layout/list_item.xml | 321 +++++++++++ .../res/layout/login_flow_info_layout_v2.xml | 102 ++++ app/src/gplay/res/layout/media_control.xml | 121 ++++ ...ring_permissions_bottom_sheet_fragment.xml | 27 + .../settings_preference_first_group.xml | 26 + .../res/layout/settings_preference_group.xml | 25 + .../layout/settings_preference_list_item.xml | 34 ++ .../layout/settings_preference_switcher.xml | 55 ++ .../res/layout/settings_privacy_switchers.xml | 69 +++ app/src/gplay/res/layout/share_activity.xml | 85 +++ ...ile_list_actions_bottom_sheet_fragment.xml | 203 +++++++ .../gplay/res/layout/simple_spinner_item.xml | 16 + .../res/layout/sorting_order_fragment.xml | 133 +++++ .../res/layout/synced_folders_item_header.xml | 109 ++++ app/src/gplay/res/layout/toolbar_standard.xml | 251 ++++++++ .../res/layout/view_authorization_method.xml | 92 +++ .../res/layout/view_login_background.xml | 26 + .../gplay/res/menu/partial_drawer_entries.xml | 140 +++++ .../gplay/res/values-land/ionos-dimens.xml | 15 + .../gplay/res/values-large/ionos-dimens.xml | 30 + app/src/gplay/res/values-v27/ionos-styles.xml | 22 + app/src/gplay/res/values/ionos-dimens.xml | 85 +++ app/src/gplay/res/values/ionos-styles.xml | 304 ++++++++++ app/src/gplay/res/values/player-colors.xml | 16 + app/src/gplay/res/values/setup.xml | 22 - app/src/gplay/res/xml/preferences.xml | 212 +++++++ .../analytics/FirebaseAnalyticsManager.kt | 12 + app/src/main/AndroidManifest.xml | 22 + app/src/main/assets/branding/config.json | 8 + .../com/ionos/analytics/AnalyticsManager.kt | 12 + .../ionos/annotation/IonosCustomization.kt | 24 + .../AuthorizationMethodActivity.kt | 41 ++ .../java/com/ionos/branding/BrandConfig.kt | 38 ++ .../com/ionos/branding/BrandConfigProvider.kt | 43 ++ .../main/java/com/ionos/di/StratoModule.kt | 35 ++ .../java/com/ionos/player/PlayerModule.kt | 100 ++++ .../media3/MediaNotificationProvider.kt | 26 + .../ionos/player/media3/PlaybackModelImpl.kt | 228 ++++++++ .../media3/PlaybackModelPlayerListener.kt | 89 +++ .../ionos/player/media3/PlaybackService.kt | 75 +++ .../player/media3/PlaybackStateFactory.kt | 89 +++ .../player/media3/common/MediaItemFactory.kt | 33 ++ .../player/media3/common/MediaMetadata.kt | 21 + .../player/media3/common/PlayerFactory.kt | 14 + .../media3/controller/MediaController.kt | 61 ++ .../controller/MediaControllerFactory.kt | 29 + .../media3/datasource/DataSourceFactory.kt | 54 ++ .../media3/datasource/StreamDataSource.kt | 80 +++ .../media3/exoplayer/ExoPlayerFactory.kt | 36 ++ .../media3/exoplayer/MediaSourceFactory.kt | 46 ++ .../resumption/PlaybackResumptionConfig.kt | 18 + .../PlaybackResumptionConfigStore.kt | 71 +++ .../PlaybackResumptionPlayerListener.kt | 21 + .../session/MediaSessionActivityFactory.kt | 39 ++ .../session/MediaSessionBitmapLoader.kt | 111 ++++ .../media3/session/MediaSessionCallback.kt | 119 ++++ .../media3/session/MediaSessionFactory.kt | 58 ++ .../media3/session/MediaSessionHolder.kt | 17 + .../com/ionos/player/model/PlaybackFile.kt | 24 + .../ionos/player/model/PlaybackFileMapper.kt | 40 ++ .../ionos/player/model/PlaybackFileType.kt | 13 + .../player/model/PlaybackFileUriMapper.kt | 36 ++ .../com/ionos/player/model/PlaybackFiles.kt | 13 + .../player/model/PlaybackFilesComparator.kt | 63 ++ .../player/model/PlaybackFilesRepository.kt | 162 ++++++ .../com/ionos/player/model/PlaybackModel.kt | 58 ++ .../model/PlaybackModelCompositeListener.kt | 36 ++ .../ionos/player/model/PlaybackSettings.kt | 46 ++ .../com/ionos/player/model/SourceException.kt | 14 + .../com/ionos/player/model/ThumbnailLoader.kt | 96 ++++ .../com/ionos/player/model/VideoViewSetter.kt | 14 + .../DefaultPlaybackErrorStrategy.kt | 22 + .../error_strategy/PlaybackErrorStrategy.kt | 15 + .../model/state/PlaybackItemMetadata.kt | 19 + .../player/model/state/PlaybackItemState.kt | 20 + .../ionos/player/model/state/PlaybackState.kt | 18 + .../ionos/player/model/state/PlayerState.kt | 18 + .../player/model/state/PlayerStateAnalyzer.kt | 30 + .../ionos/player/model/state/RepeatMode.kt | 16 + .../com/ionos/player/model/state/VideoSize.kt | 15 + .../com/ionos/player/ui/PlayerActivity.kt | 253 +++++++++ .../com/ionos/player/ui/PlayerLauncher.kt | 63 ++ .../com/ionos/player/ui/PlayerScreenEvent.kt | 27 + .../java/com/ionos/player/ui/PlayerView.kt | 117 ++++ .../com/ionos/player/ui/PlayerViewModel.kt | 124 ++++ .../player/ui/audio/AudioFileFragment.kt | 130 +++++ .../ui/audio/AudioFileFragmentFactory.kt | 19 + .../ionos/player/ui/audio/AudioPlayerView.kt | 46 ++ .../player/ui/common/PlayerBasePresenter.kt | 19 + .../ui/common/PlayerProgressIndicator.kt | 84 +++ .../ionos/player/ui/control/PlayerControl.kt | 61 ++ .../ui/control/PlayerControlPresenter.kt | 161 ++++++ .../player/ui/control/PlayerControlView.java | 329 +++++++++++ .../listener/MultipleClickListener.java | 52 ++ .../PlayerControlViewCompositeListener.java | 94 +++ .../listener/PlayerControlViewListener.java | 31 + .../ionos/player/ui/pager/PlayerPager.java | 332 +++++++++++ .../ui/pager/PlayerPagerFragmentFactory.java | 14 + .../player/ui/pager/PlayerPagerMode.java | 13 + .../adapter/AbstractFragmentPagerAdapter.java | 101 ++++ .../adapter/DefaultFragmentPagerAdapter.java | 47 ++ .../adapter/InfiniteFragmentPagerAdapter.java | 88 +++ .../listener/NullPlayerPagerListener.java | 21 + .../OnPageChangeCompositeListener.java | 47 ++ .../pager/listener/PlayerPagerListener.java | 12 + .../player/ui/video/VideoFileFragment.kt | 132 +++++ .../ui/video/VideoFileFragmentFactory.kt | 19 + .../ionos/player/ui/video/VideoPlayerView.kt | 102 ++++ .../java/com/ionos/player/util/Action.java | 12 + .../com/ionos/player/util/ContentResolver.kt | 26 + .../java/com/ionos/player/util/Context.kt | 38 ++ .../com/ionos/player/util/PeriodicAction.java | 39 ++ .../java/com/ionos/player/util/ScreenUtils.kt | 47 ++ .../com/ionos/player/util/SystemVersion.java | 40 ++ .../com/ionos/player/util/WindowWrapper.kt | 55 ++ .../ionos/privacy/DataProtectionActivity.kt | 150 +++++ .../ionos/privacy/DataProtectionViewModel.kt | 81 +++ .../com/ionos/privacy/PrivacyPreferences.kt | 59 ++ .../ionos/privacy/PrivacySettingsActivity.kt | 83 +++ .../ionos/privacy/PrivacySettingsViewModel.kt | 50 ++ .../com/ionos/scanbot/di/NCScanbotModule.kt | 46 ++ .../scanbot/image_loading/ImageLoaderImpl.kt | 22 + .../image_loading/ImageRequestBuilderImpl.kt | 42 ++ .../scanbot/image_loading/ObjectKey.java | 49 ++ .../license/DownloadLicenseRemoteOperation.kt | 59 ++ .../license/LicenseResponseTransformer.kt | 39 ++ .../scanbot/license/LoadScanbotLicenseImpl.kt | 42 ++ .../license/ScanbotLicenseDownloadWorker.kt | 86 +++ .../license/ScanbotLicenseJobFactory.kt | 39 ++ .../com/ionos/scanbot/logger/ScanbotLogger.kt | 28 + .../ui/ScanbotCameraPermissionHandler.kt | 60 ++ .../ionos/scanbot/upload/ScanbotUploader.kt | 39 ++ .../upload/SelectDirectoryContractImpl.kt | 42 ++ .../scanbot/utils/GetLocalFreeSpaceImpl.kt | 16 + .../com/ionos/startup/IonosInitializer.kt | 43 ++ .../ionos/utils/IonosAndroidViewThemeUtils.kt | 220 +++++++ .../utils/IonosAndroidXViewThemeUtils.kt | 52 ++ .../java/com/ionos/utils/IonosBuildHelper.kt | 27 + .../ionos/utils/IonosDialogViewThemeUtils.kt | 67 +++ .../utils/IonosFilesSpecificViewThemeUtils.kt | 103 ++++ .../utils/IonosMaterialViewThemeUtils.kt | 159 ++++++ .../com/ionos/utils/context/ContextUtils.kt | 18 + .../com/ionos/utils/text/CharSequenceUtils.kt | 46 ++ .../account/UserAccountManagerImpl.java | 5 +- .../nextcloud/client/database/dao/FileDao.kt | 20 +- .../com/nextcloud/client/di/AppComponent.java | 4 + .../com/nextcloud/client/di/VariantModule.kt | 36 +- .../client/editimage/EditImageActivity.kt | 9 +- .../client/jobs/AccountRemovalWork.kt | 6 +- .../client/jobs/BackgroundJobFactory.kt | 9 +- .../java/com/nextcloud/client/media/Player.kt | 4 +- .../nextcloud/client/media/PlayerService.kt | 12 + .../nextcloud/client/mixins/SessionMixin.kt | 9 +- .../preferences/AppPreferencesImpl.java | 8 + .../ui/ChooseAccountDialogFragment.kt | 3 + .../nextcloud/ui/fileactions/FileAction.kt | 21 +- .../ui/fileactions/FileActionsBottomSheet.kt | 16 +- .../java/com/nextcloud/utils/BuildHelper.kt | 7 +- .../com/nmc/android/ui/LauncherActivity.kt | 23 +- .../authentication/AuthenticatorActivity.java | 198 +++++-- .../IonosLoginDomainResolver.java | 51 ++ .../datamodel/FileDataStorageManager.java | 43 ++ .../owncloud/android/datamodel/OCFile.java | 4 + .../datamodel/ThumbnailsCacheManager.java | 11 +- .../android/media/MediaControlView.kt | 37 +- .../operations/CreateFolderOperation.java | 1 + .../providers/FileContentProvider.java | 49 ++ .../UsersAndGroupsSearchProvider.java | 7 +- .../ui/RoundedCornersConstraintLayout.kt | 25 + .../owncloud/android/ui/SquareFrameLayout.kt | 23 + .../ui/activity/AnimatedDrawerListener.java | 80 +++ .../android/ui/activity/BaseActivity.java | 12 +- .../android/ui/activity/DrawerActivity.java | 38 +- .../ui/activity/ExternalSiteWebView.java | 4 + .../android/ui/activity/FileActivity.java | 9 + .../ui/activity/FileDisplayActivity.kt | 30 +- .../ui/activity/FolderPickerActivity.kt | 25 + .../ui/activity/ManageAccountsActivity.kt | 9 +- .../ReceiveExternalFilesActivity.java | 19 +- .../android/ui/activity/SettingsActivity.java | 65 ++- .../android/ui/activity/ShareActivity.kt | 10 +- .../SingleStateDrawerArrowDrawable.kt | 24 + .../ui/activity/SyncedFoldersActivity.kt | 3 + .../android/ui/activity/ToolbarActivity.java | 21 +- .../ui/activity/UploadFilesActivity.java | 5 +- .../android/ui/activity/UploadListActivity.kt | 4 +- .../android/ui/activity/UserInfoActivity.java | 4 +- .../ui/adapter/FileDetailTabAdapter.kt | 27 +- .../android/ui/adapter/GalleryRowHolder.kt | 54 +- .../android/ui/adapter/LinkShareViewHolder.kt | 8 +- .../ui/adapter/ListGridItemViewHolder.kt | 4 + .../ui/adapter/LocalFileListAdapter.java | 6 +- .../android/ui/adapter/OCFileListAdapter.java | 36 +- .../android/ui/adapter/OCFileListDelegate.kt | 61 +- .../adapter/OCFileListGridItemViewHolder.kt | 5 + .../ui/adapter/OCFileListItemViewHolder.kt | 5 + .../OCFileListRecommendedItemViewHolder.kt | 3 + .../ui/adapter/OCFileListThumbnailLoader.kt | 157 +++++ .../ui/adapter/OCShareToOCFileConverter.kt | 3 + .../adapter/QuickSharingPermissionsAdapter.kt | 2 + .../android/ui/adapter/ShareViewHolder.java | 12 +- .../android/ui/adapter/ShareeListAdapter.kt | 5 +- .../android/ui/adapter/SyncedFolderAdapter.kt | 5 +- .../ui/adapter/TrashbinListAdapter.java | 14 +- .../UnifiedSearchCurrentDirItemViewHolder.kt | 2 + .../ui/adapter/UnifiedSearchItemViewHolder.kt | 20 +- .../android/ui/adapter/UserListAdapter.java | 3 + .../android/ui/dialog/AccountRemovalDialog.kt | 14 +- .../android/ui/dialog/LoadingDialog.kt | 25 +- .../ui/dialog/SortingOrderDialogFragment.kt | 4 +- .../dialog/StoragePermissionDialogFragment.kt | 8 +- .../ui/fragment/ExtendedListFragment.kt | 48 +- .../ui/fragment/FileDetailFragment.java | 21 +- .../ui/fragment/FileDetailSharingFragment.kt | 7 +- ...ileDetailSharingMenuBottomSheetDialog.java | 19 +- .../ui/fragment/FileListLayoutManager.kt | 4 +- .../android/ui/fragment/GalleryFragment.kt | 5 +- .../GalleryFragmentBottomSheetDialog.kt | 17 +- .../ui/fragment/LocalFileListFragment.java | 18 +- .../ui/fragment/OCFileListFragment.java | 58 +- ...ckSharingPermissionsBottomSheetDialog.java | 7 +- .../SimpleOCFileListBottomSheetDialog.java | 121 ++++ .../ui/fragment/UnifiedSearchFragment.kt | 15 +- .../UnifiedSearchFragmentScreenState.kt | 4 +- .../fragment/contactsbackup/BackupFragment.kt | 23 +- .../ui/interfaces/CompletionCallback.kt | 12 + .../ui/preview/PreviewImageActivity.kt | 24 +- .../ui/preview/PreviewImageFragment.kt | 9 +- .../ui/preview/PreviewMediaActivity.kt | 63 +- .../ui/preview/PreviewMediaPagerAdapter.kt | 4 +- .../ui/preview/PreviewTextStringFragment.kt | 3 +- .../android/ui/trashbin/TrashbinFragment.kt | 7 +- .../owncloud/android/utils/DisplayUtils.java | 79 +++ .../owncloud/android/utils/DrawableUtil.kt | 38 +- .../android/utils/DrawerMenuUtil.java | 6 + .../android/utils/EncryptionUtilsV2.kt | 1 + .../owncloud/android/utils/MimeTypeUtil.java | 16 +- .../android/utils/theme/ViewThemeUtils.kt | 28 +- app/src/main/res/anim/fade_in.xml | 12 + app/src/main/res/anim/fade_out.xml | 12 + .../res/color-night/radio_button_color.xml | 11 + .../main/res/color/fab_background_tint.xml | 13 + .../main/res/color/filled_button_bg_color.xml | 14 + .../res/color/filled_button_text_color.xml | 14 + .../res/color/outlined_button_bg_color.xml | 14 + .../color/outlined_button_stroke_color.xml | 14 + .../res/color/outlined_button_text_color.xml | 14 + app/src/main/res/color/radio_button_color.xml | 11 + .../main/res/color/text_button_text_color.xml | 14 + .../player_ic_notification_audio.xml | 21 + .../player_ic_notification_video.xml | 21 + .../main/res/drawable/background_splash.xml | 17 + app/src/main/res/drawable/file_pdf.xml | 4 +- .../drawable/grid_mode_item_background.xml | 11 + .../grid_mode_selected_item_background.xml | 11 + .../main/res/drawable/ic_data_protection.xml | 17 + .../drawable/ic_file_action_cancel_sync.xml | 10 + .../main/res/drawable/ic_file_action_edit.xml | 10 + .../res/drawable/ic_file_action_lock_file.xml | 10 + .../drawable/ic_file_action_move_or_copy.xml | 10 + .../ic_file_action_open_file_with.xml | 10 + .../drawable/ic_file_action_set_encrypted.xml | 10 + .../drawable/ic_file_action_stream_media.xml | 10 + .../res/drawable/ic_file_action_sync_file.xml | 10 + .../drawable/ic_file_action_unlock_file.xml | 10 + .../ic_file_action_unset_encrypted.xml | 10 + .../drawable/ic_folder_all_share_types.xml | 26 + .../main/res/drawable/ic_shared_all_types.xml | 17 + app/src/main/res/drawable/player_ic_audio.xml | 16 + app/src/main/res/drawable/player_ic_close.xml | 16 + .../drawable/player_ic_notification_audio.xml | 15 + .../drawable/player_ic_notification_video.xml | 15 + app/src/main/res/drawable/player_ic_pause.xml | 23 + app/src/main/res/drawable/player_ic_play.xml | 23 + .../main/res/drawable/player_ic_repeat.xml | 16 + .../main/res/drawable/player_ic_shuffle.xml | 16 + .../main/res/drawable/player_ic_skip_next.xml | 16 + .../res/drawable/player_ic_skip_previous.xml | 16 + app/src/main/res/drawable/player_ic_video.xml | 16 + .../res/drawable/player_progress_drawable.xml | 25 + .../res/drawable/player_progress_thumb.xml | 16 + .../main/res/drawable/rounded_rect_4dp.xml | 11 + .../main/res/layout-land/account_setup.xml | 3 +- app/src/main/res/layout/account_item.xml | 2 +- .../res/layout/account_removal_dialog.xml | 2 + .../res/layout/activity_data_protection.xml | 24 + .../activity_data_protection_detail_page.xml | 71 +++ ...activity_data_protection_overview_page.xml | 132 +++++ .../main/res/layout/activity_edit_image.xml | 6 +- .../main/res/layout/activity_list_item.xml | 2 +- .../res/layout/activity_list_item_header.xml | 2 +- .../res/layout/activity_privacy_settings.xml | 43 ++ app/src/main/res/layout/deep_link_login.xml | 2 +- .../main/res/layout/dialog_app_passcode.xml | 7 + .../main/res/layout/dialog_choose_account.xml | 8 +- .../res/layout/dialog_theme_selection.xml | 7 + app/src/main/res/layout/drawer_header.xml | 2 +- .../main/res/layout/file_details_fragment.xml | 4 +- ...details_share_public_link_add_new_item.xml | 3 +- .../layout/file_details_share_share_item.xml | 8 +- .../layout/file_details_sharing_fragment.xml | 4 +- .../file_details_sharing_process_fragment.xml | 47 +- app/src/main/res/layout/file_thumbnail.xml | 7 +- app/src/main/res/layout/files.xml | 5 + .../main/res/layout/files_folder_picker.xml | 8 +- app/src/main/res/layout/gallery_header.xml | 5 +- app/src/main/res/layout/grid_item.xml | 9 + app/src/main/res/layout/ionos_item_media.xml | 40 ++ app/src/main/res/layout/list_item.xml | 7 + .../main/res/layout/log_entry_list_item.xml | 1 + .../res/layout/login_flow_info_layout_v2.xml | 14 + .../res/layout/player_audio_file_fragment.xml | 74 +++ app/src/main/res/layout/player_audio_view.xml | 71 +++ .../main/res/layout/player_control_view.xml | 79 +++ app/src/main/res/layout/player_pager.xml | 11 + .../res/layout/player_video_file_fragment.xml | 32 ++ app/src/main/res/layout/player_video_view.xml | 80 +++ .../main/res/layout/richdocuments_webview.xml | 2 +- .../main/res/layout/send_share_fragment.xml | 4 +- .../res/layout/settings_privacy_switchers.xml | 62 ++ ...ile_list_actions_bottom_sheet_fragment.xml | 208 +++++++ .../main/res/layout/simple_spinner_item.xml | 15 + app/src/main/res/layout/storage_path_item.xml | 5 +- .../layout/synced_folders_settings_layout.xml | 12 +- app/src/main/res/layout/trashbin_item.xml | 4 +- .../unified_search_current_directory_item.xml | 14 + .../main/res/layout/unified_search_item.xml | 13 + .../main/res/layout/upload_files_layout.xml | 5 +- app/src/main/res/layout/upload_list_item.xml | 2 +- .../res/layout/uploader_list_item_layout.xml | 2 +- app/src/main/res/layout/user_info_layout.xml | 4 +- .../res/layout/view_authorization_method.xml | 25 + .../main/res/menu/activity_folder_picker.xml | 2 +- .../menu/activity_receive_external_files.xml | 2 +- app/src/main/res/values-de/ionos-strings.xml | 35 ++ app/src/main/res/values-es-rCR/strings.xml | 535 +++++++++++++++++ app/src/main/res/values-es-rGT/strings.xml | 536 ++++++++++++++++++ app/src/main/res/values-es-rSV/strings.xml | 536 ++++++++++++++++++ app/src/main/res/values-es/ionos-strings.xml | 35 ++ app/src/main/res/values-fr/ionos-strings.xml | 35 ++ app/src/main/res/values-it/ionos-strings.xml | 36 ++ .../main/res/values-land/ionos-integers.xml | 11 + .../main/res/values-land/player-dimens.xml | 18 + .../res/values-large-land/ionos-integers.xml | 11 + .../res/values-large-land/player-dimens.xml | 18 + .../main/res/values-large/ionos-dimens.xml | 29 + .../main/res/values-large/ionos-integers.xml | 11 + .../main/res/values-large/player-dimens.xml | 21 + app/src/main/res/values-night/colors.xml | 2 +- .../main/res/values-night/ionos-colors.xml | 49 ++ .../res/values-night/ionos-scanbot-theme.xml | 47 ++ app/src/main/res/values-nl/ionos-strings.xml | 35 ++ app/src/main/res/values/dims.xml | 1 + app/src/main/res/values/ionos-attrs.xml | 11 + app/src/main/res/values/ionos-bools.xml | 11 + .../main/res/values/ionos-colors-palette.xml | 40 ++ app/src/main/res/values/ionos-colors.xml | 77 +++ app/src/main/res/values/ionos-dimens.xml | 37 ++ app/src/main/res/values/ionos-integers.xml | 11 + .../main/res/values/ionos-scanbot-theme.xml | 47 ++ app/src/main/res/values/ionos-strings.xml | 35 ++ app/src/main/res/values/ionos-styles.xml | 43 ++ app/src/main/res/values/ionos-themes.xml | 30 + .../main/res/values/player-colors-palette.xml | 12 + app/src/main/res/values/player-colors.xml | 18 + app/src/main/res/values/player-dimens.xml | 36 ++ app/src/main/res/values/player-strings.xml | 14 + app/src/main/res/values/player-styles.xml | 37 ++ app/src/main/res/values/strings.xml | 37 +- app/src/main/res/values/styles.xml | 21 +- .../res/xml/users_and_groups_searchable.xml | 2 +- .../analytics/FirebaseAnalyticsManager.kt | 12 + .../DefaultPlaybackErrorStrategyTest.java | 80 +++ .../player/ui/pager/PlayerPagerTest.java | 58 ++ .../client/jobs/BackgroundJobFactoryTest.kt | 10 + .../IonosLoginDomainResolverTest.java | 156 +++++ .../analytics/FirebaseAnalyticsManager.kt | 12 + appscan/build.gradle.kts | 1 + build.gradle.kts | 2 + .../android/en-US/changelogs/30300053.txt | 7 + .../en-US/changelogs/30300053.txt.license | 2 + .../android/en-US/changelogs/30300090.txt | 7 + .../en-US/changelogs/30300090.txt.license | 2 + .../android/en-US/changelogs/30300151.txt | 7 + .../en-US/changelogs/30300151.txt.license | 2 + .../android/en-US/changelogs/30300190.txt | 7 + .../en-US/changelogs/30300190.txt.license | 2 + .../android/en-US/changelogs/30300290.txt | 7 + .../en-US/changelogs/30300290.txt.license | 2 + .../android/en-US/changelogs/30300390.txt | 7 + .../en-US/changelogs/30300390.txt.license | 2 + .../android/en-US/changelogs/30300490.txt | 10 + .../en-US/changelogs/30300490.txt.license | 2 + .../android/en-US/changelogs/30300590.txt | 9 + .../en-US/changelogs/30300590.txt.license | 2 + .../android/en-US/changelogs/30300690.txt | 8 + .../en-US/changelogs/30300690.txt.license | 2 + .../android/en-US/changelogs/30300790.txt | 7 + .../en-US/changelogs/30300790.txt.license | 2 + .../android/en-US/changelogs/30310051.txt | 15 + .../en-US/changelogs/30310051.txt.license | 2 + .../android/en-US/changelogs/30310052.txt | 11 + .../en-US/changelogs/30310052.txt.license | 2 + .../android/en-US/changelogs/30310090.txt | 8 + .../en-US/changelogs/30310090.txt.license | 2 + .../android/en-US/changelogs/30310151.txt | 7 + .../en-US/changelogs/30310151.txt.license | 2 + .../android/en-US/changelogs/30310190.txt | 7 + .../en-US/changelogs/30310190.txt.license | 2 + .../android/en-US/changelogs/30310251.txt | 7 + .../en-US/changelogs/30310251.txt.license | 2 + .../android/en-US/changelogs/30310290.txt | 7 + .../en-US/changelogs/30310290.txt.license | 2 + .../android/en-US/changelogs/30310351.txt | 9 + .../en-US/changelogs/30310351.txt.license | 2 + .../android/en-US/changelogs/30310390.txt | 9 + .../en-US/changelogs/30310390.txt.license | 2 + .../android/en-US/changelogs/30310490.txt | 7 + .../en-US/changelogs/30310490.txt.license | 2 + .../android/en-US/changelogs/30320051.txt | 11 + .../en-US/changelogs/30320051.txt.license | 2 + .../android/en-US/changelogs/30320090.txt | 11 + .../en-US/changelogs/30320090.txt.license | 2 + .../android/en-US/changelogs/30320190.txt | 7 + .../en-US/changelogs/30320190.txt.license | 2 + .../android/en-US/changelogs/30320290.txt | 9 + .../en-US/changelogs/30320290.txt.license | 2 + .../android/en-US/changelogs/30320351.txt | 16 + .../en-US/changelogs/30320351.txt.license | 2 + .../android/en-US/changelogs/30320352.txt | 10 + .../en-US/changelogs/30320352.txt.license | 2 + .../android/en-US/changelogs/30320353.txt | 9 + .../en-US/changelogs/30320353.txt.license | 2 + .../android/en-US/changelogs/30320390.txt | 7 + .../en-US/changelogs/30320390.txt.license | 2 + .../android/en-US/changelogs/30340051.txt | 9 + .../en-US/changelogs/30340051.txt.license | 2 + .../android/en-US/changelogs/30340052.txt | 9 + .../en-US/changelogs/30340052.txt.license | 2 + .../android/en-US/changelogs/30340090.txt | 7 + .../en-US/changelogs/30340090.txt.license | 2 + .../android/en-US/changelogs/30340151.txt | 9 + .../en-US/changelogs/30340151.txt.license | 2 + .../android/en-US/changelogs/30340190.txt | 9 + .../en-US/changelogs/30340190.txt.license | 2 + .../android/en-US/changelogs/30350051.txt | 12 + .../en-US/changelogs/30350051.txt.license | 2 + .../android/en-US/changelogs/30350052.txt | 7 + .../en-US/changelogs/30350052.txt.license | 2 + .../android/en-US/changelogs/30350053.txt | 9 + .../en-US/changelogs/30350053.txt.license | 2 + .../android/en-US/changelogs/30350054.txt | 7 + .../en-US/changelogs/30350054.txt.license | 2 + .../android/en-US/changelogs/30350090.txt | 9 + .../en-US/changelogs/30350090.txt.license | 2 + .../android/en-US/changelogs/30350190.txt | 7 + .../en-US/changelogs/30350190.txt.license | 2 + .../android/en-US/changelogs/30350290.txt | 7 + .../en-US/changelogs/30350290.txt.license | 2 + .../android/en-US/changelogs/30360051.txt | 9 + .../en-US/changelogs/30360051.txt.license | 2 + .../android/en-US/changelogs/30360052.txt | 8 + .../en-US/changelogs/30360052.txt.license | 2 + .../en-US/changelogs/330000090.license | 2 + .../android/en-US/changelogs/330000090.txt | 8 + .../en-US/changelogs/330000151.license | 2 + .../android/en-US/changelogs/330000151.txt | 8 + .../android/en-US/changelogs/330000190.txt | 8 + .../en-US/changelogs/330000190.txt.license | 2 + .../android/en-US/changelogs/330010051.txt | 11 + .../en-US/changelogs/330010051.txt.license | 2 + .../android/en-US/changelogs/330010052.txt | 8 + .../en-US/changelogs/330010052.txt.license | 2 + .../android/en-US/changelogs/330010053.txt | 7 + .../en-US/changelogs/330010053.txt.license | 2 + .../android/en-US/changelogs/330010090.txt | 7 + .../en-US/changelogs/330010090.txt.license | 2 + .../android/en-US/changelogs/330010151.txt | 9 + .../en-US/changelogs/330010151.txt.license | 2 + .../android/en-US/changelogs/330010152.txt | 9 + .../en-US/changelogs/330010152.txt.license | 2 + .../android/en-US/changelogs/330010190.txt | 9 + .../en-US/changelogs/330010190.txt.license | 2 + .../android/en-US/changelogs/330010290.txt | 7 + .../en-US/changelogs/330010290.txt.license | 2 + .../android/en-US/changelogs/340000051.txt | 10 + .../en-US/changelogs/340000051.txt.license | 2 + .../android/en-US/changelogs/340000090.txt | 10 + .../en-US/changelogs/340000090.txt.license | 2 + .../android/en-US/changelogs/340000151.txt | 8 + .../en-US/changelogs/340000151.txt.license | 2 + .../android/en-US/changelogs/340000190.txt | 7 + .../en-US/changelogs/340000190.txt.license | 2 + gradle.properties | 7 + gradle/libs.versions.toml | 19 +- gradle/verification-metadata.xml | 404 ++++++++++++- scanbot/.gitignore | 2 + scanbot/build.gradle.kts | 88 +++ scanbot/jacoco.exec | Bin 0 -> 1838 bytes scanbot/proguard-rules.pro | 27 + scanbot/src/main/AndroidManifest.xml | 48 ++ .../src/main/assets/ocr_blobs/eng.traineddata | Bin 0 -> 4113088 bytes .../src/main/assets/ocr_blobs/fra.traineddata | Bin 0 -> 1130365 bytes .../src/main/assets/ocr_blobs/osd.traineddata | Bin 0 -> 10562727 bytes .../scanbot/availability/Availability.java | 13 + .../ScanbotFeatureAvailability.kt | 19 + .../ScanbotLicenseAvailability.kt | 18 + .../ionos/scanbot/cleaner/TempFilesCleaner.kt | 24 + .../scanbot/controller/ScanbotController.kt | 36 ++ .../controller/ScanbotControllerImpl.kt | 86 +++ .../ionos/scanbot/controller/StateManager.kt | 91 +++ .../com/ionos/scanbot/di/ScanbotModule.kt | 103 ++++ .../com/ionos/scanbot/di/ScanbotTracking.kt | 57 ++ .../ionos/scanbot/di/qualifiers/Scanbot.kt | 16 + .../scanbot/di/qualifiers/ScanbotLicense.kt | 16 + .../di/qualifiers/ScanbotLicenseKey.kt | 16 + .../di/qualifiers/ScanbotLicenseKeyUrl.kt | 16 + .../com/ionos/scanbot/entity/ImageFormat.kt | 18 + .../ionos/scanbot/entity/ModifiedPicture.kt | 11 + .../ionos/scanbot/entity/OriginalPicture.kt | 58 ++ .../java/com/ionos/scanbot/entity/Picture.kt | 33 ++ .../ionos/scanbot/entity/SelectedContour.kt | 24 + .../exception/CreateIntentException.kt | 14 + .../exception/DecodeBitmapException.kt | 10 + .../exception/ImportPictureException.kt | 12 + .../exception/InvalidFileNameException.kt | 10 + .../exception/NoCameraPermissionException.kt | 12 + .../exception/NoFreeLocalSpaceException.java | 21 + .../scanbot/exception/OpenFileException.kt | 12 + .../exception/PictureNotFoundException.kt | 10 + .../exception/ReadPictureBitmapException.kt | 10 + .../exception/RecognizeDocumentException.kt | 10 + .../exception/RenderDocumentException.kt | 10 + .../exception/SaveDocumentException.kt | 10 + .../java/com/ionos/scanbot/filter/Filter.kt | 14 + .../com/ionos/scanbot/filter/FilterType.kt | 12 + .../com/ionos/scanbot/filter/Filterable.kt | 21 + .../ionos/scanbot/filter/color/ColorFilter.kt | 33 ++ .../scanbot/filter/color/ColorFilterType.kt | 194 +++++++ .../ionos/scanbot/filter/crop/CropFilter.kt | 23 + .../scanbot/filter/rotate/RotateFilter.kt | 16 + .../scanbot/image_loader/ImageLoader.java | 19 + .../image_loader/ImageLoaderOptions.kt | 12 + .../image_loader/ImageRequestBuilder.java | 20 + .../ionos/scanbot/image_loader/ScaleType.java | 13 + .../scanbot/initializer/ScanbotInitializer.kt | 17 + .../initializer/ScanbotInitializerImpl.kt | 55 ++ .../initializer/TryToInitScanbotSdk.kt | 34 ++ .../com/ionos/scanbot/license/KeyStore.kt | 15 + .../ionos/scanbot/license/LicenseKeyStore.kt | 27 + .../scanbot/license/LoadScanbotLicense.kt | 14 + .../scanbot/license/PreferencesKeyStore.kt | 35 ++ .../algorithm/AesGcmEncryptionAlgorithm.java | 56 ++ .../AlgorithmParameterSpecFactory.java | 15 + .../AlgorithmParameterSpecFactoryImpl.java | 26 + .../algorithm/EncryptionAlgorithm.java | 25 + .../license/oath/security/key/AesGcmKey.java | 19 + .../oath/security/key/AesGcmKeyGenerator.java | 27 + .../security/key/AesGcmKeyRepository.java | 81 +++ .../license/oath/settings/SecureEditor.java | 87 +++ .../oath/settings/SecureEncryptor.java | 35 ++ .../settings/SecureSharedPreferences.java | 157 +++++ .../network/NetworkInfoIsAbsentException.java | 15 + .../scanbot/network/NetworkStateBundle.kt | 15 + .../network/NetworkStateChangeReceiver.java | 50 ++ .../ionos/scanbot/network/NetworkUtils.java | 104 ++++ .../provider/ContourDetectorParamsProvider.kt | 13 + .../provider/ContourDetectorProvider.kt | 27 + .../scanbot/provider/DocumentNameProvider.kt | 29 + .../ionos/scanbot/provider/FileProvider.kt | 33 ++ .../com/ionos/scanbot/provider/SdkProvider.kt | 22 + .../provider/SupportedLanguagesProvider.kt | 21 + .../scanbot/repository/PictureRepository.kt | 82 +++ .../scanbot/repository/RepositoryFacade.kt | 94 +++ .../repository/bitmap/BitmapDecoder.kt | 99 ++++ .../repository/bitmap/BitmapRepository.kt | 59 ++ .../scanbot/repository/bitmap/BitmapSaver.kt | 93 +++ .../scanbot/screens/base/BaseActivity.kt | 126 ++++ .../ionos/scanbot/screens/base/BaseScreen.kt | 27 + .../scanbot/screens/base/BaseViewModel.kt | 44 ++ .../screens/base/BaseWindowInsetsActivity.kt | 50 ++ .../scanbot/screens/camera/CameraActivity.kt | 189 ++++++ .../scanbot/screens/camera/CameraScreen.kt | 65 +++ .../scanbot/screens/camera/CameraViewModel.kt | 205 +++++++ .../screens/camera/CameraViewModelFactory.kt | 22 + .../image_picker/ImagePickerContract.kt | 34 ++ .../image_picker/ImagePickerLauncher.kt | 42 ++ .../ui_components/CameraViewController.kt | 182 ++++++ .../ui_components/ImportProgressDialog.kt | 72 +++ .../NoFreeSpaceMessageDialogWrapper.kt | 31 + .../camera/ui_components/UserGuidanceView.kt | 75 +++ .../camera/use_case/AddPictureToRepository.kt | 27 + .../use_case/ExtractPictureTransformation.kt | 26 + .../use_case/GetCameraScreenErrorMessage.kt | 37 ++ .../camera/use_case/GetUserGuidanceStatus.kt | 40 ++ .../camera/use_case/ReadContentToByteArray.kt | 25 + .../camera/use_case/UserGuidanceStatus.kt | 10 + .../import_pictures/ImportPictures.kt | 73 +++ .../ImportPicturesInputItem.kt | 17 + .../import_pictures/ImportPicturesState.kt | 36 ++ .../scanbot/screens/common/ExitDialog.kt | 40 ++ .../screens/common/LockProgressDialog.kt | 52 ++ .../screens/common/use_case/SelectContour.kt | 27 + .../common/use_case/open_screen/OpenScreen.kt | 37 ++ .../use_case/open_screen/OpenScreenIntent.kt | 56 ++ .../scanbot/screens/crop/CropActivity.kt | 103 ++++ .../ionos/scanbot/screens/crop/CropScreen.kt | 46 ++ .../scanbot/screens/crop/CropViewModel.kt | 172 ++++++ .../screens/crop/CropViewModelFactory.kt | 44 ++ .../scanbot/screens/crop/view/CropView.kt | 83 +++ .../scanbot/screens/filter/FilterActivity.kt | 122 ++++ .../scanbot/screens/filter/FilterScreen.kt | 39 ++ .../scanbot/screens/filter/FilterViewModel.kt | 167 ++++++ .../screens/filter/FilterViewModelFactory.kt | 39 ++ .../filter/use_case/GetColorFilterName.kt | 24 + .../screens/filter/view/FilterControls.kt | 135 +++++ .../filter/view/FilterControlsListener.kt | 21 + .../screens/gallery/GalleryActivity.kt | 169 ++++++ .../gallery/GalleryBottomNavigationItem.kt | 55 ++ .../scanbot/screens/gallery/GalleryScreen.kt | 71 +++ .../screens/gallery/GalleryViewModel.kt | 198 +++++++ .../gallery/GalleryViewModelFactory.kt | 53 ++ .../gallery/pager/GalleryPagerAdapter.kt | 70 +++ .../screens/gallery/pager/GalleryPagerItem.kt | 16 + .../gallery/use_case/GetColorFilterIcon.kt | 43 ++ .../screens/rearrange/RearrangeActivity.kt | 95 ++++ .../screens/rearrange/RearrangeScreen.kt | 34 ++ .../screens/rearrange/RearrangeViewModel.kt | 83 +++ .../rearrange/RearrangeViewModelFactory.kt | 22 + .../rearrange/recycler/RearrangeAdapter.kt | 45 ++ .../rearrange/recycler/RearrangeCallback.kt | 30 + .../recycler/RearrangeDiffCallback.kt | 28 + .../rearrange/recycler/RearrangeItem.kt | 15 + .../rearrange/recycler/RearrangeViewHolder.kt | 36 ++ .../scanbot/screens/save/SaveActivity.kt | 130 +++++ .../ionos/scanbot/screens/save/SaveScreen.kt | 49 ++ .../scanbot/screens/save/SaveViewModel.kt | 145 +++++ .../screens/save/SaveViewModelFactory.kt | 22 + .../screens/save/SelectDirectoryContract.kt | 20 + .../save/use_case/ValidateFilesForUpload.kt | 27 + .../ValidateFilesForUploadSynchronous.kt | 23 + .../save/use_case/name/GetFileNames.kt | 27 + .../save/use_case/name/GetImageFileNames.kt | 38 ++ .../save/use_case/name/GetPdfFileName.kt | 22 + .../save/use_case/save/SaveDocument.kt | 27 + .../screens/save/use_case/save/SaveImages.kt | 61 ++ .../screens/save/use_case/save/SavePdf.kt | 102 ++++ .../scanbot/screens/save/view/FileTypeView.kt | 57 ++ .../ScanbotCameraScreenEventTracker.kt | 29 + .../tracking/ScanbotCropScreenEventTracker.kt | 19 + .../ScanbotFilterScreenEventTracker.kt | 43 ++ .../ScanbotGalleryScreenEventTracker.kt | 35 ++ .../ScanbotRearrangeScreenEventTracker.kt | 15 + .../tracking/ScanbotSaveScreenEventTracker.kt | 31 + .../tracking/ScanbotScreenEventTracker.kt | 12 + .../StubScanbotCameraScreenEventTracker.kt | 36 ++ .../stub/StubScanbotCropScreenEventTracker.kt | 24 + .../StubScanbotFilterScreenEventTracker.kt | 48 ++ .../StubScanbotGalleryScreenEventTracker.kt | 40 ++ .../StubScanbotRearrangeScreenEventTracker.kt | 22 + .../stub/StubScanbotSaveScreenEventTracker.kt | 37 ++ .../upload/target_provider/UploadTarget.kt | 20 + .../target_provider/UploadTargetProvider.kt | 12 + .../target_provider/UploadTargetRepository.kt | 12 + .../UploadTargetRepositoryImpl.kt | 12 + .../scanbot/upload/use_case/StartUpload.kt | 21 + .../ionos/scanbot/upload/use_case/Uploader.kt | 12 + .../com/ionos/scanbot/util/FileUtils.java | 44 ++ .../ionos/scanbot/util/GetLocalFreeSpace.kt | 12 + .../util/collections/OrderedHashMap.kt | 108 ++++ .../scanbot/util/config/Configuration.kt | 16 + .../scanbot/util/context/ContextUtils.kt | 26 + .../com/ionos/scanbot/util/graphics/Bitmap.kt | 20 + .../scanbot/util/graphics/ExifInterface.kt | 29 + .../com/ionos/scanbot/util/graphics/Matrix.kt | 36 ++ .../util/gson/RuntimeTypeAdapterFactory.java | 285 ++++++++++ .../java/com/ionos/scanbot/util/io/File.kt | 23 + .../scanbot/util/io/SaveErrorOutputStream.kt | 51 ++ .../util/lifedata/MutableStateLiveData.kt | 29 + .../scanbot/util/lifedata/StateLiveData.kt | 19 + .../com/ionos/scanbot/util/logger/Logger.kt | 14 + .../ionos/scanbot/util/logger/LoggerUtil.kt | 19 + .../util/permission/PermissionUtils.kt | 24 + .../main/java/com/ionos/scanbot/util/rx/Rx.kt | 17 + .../util/widget/DiffUtilsUpdateCallback.kt | 34 ++ .../ionos/scanbot/util/widget/ViewPager.kt | 18 + .../scanbot/util/window/WindowInsetsUtils.kt | 21 + .../scanbot/util/window/WindowWrapper.kt | 53 ++ .../color/scanbot_file_type_text_color.xml | 12 + .../scanbot_ic_crop_corner_handle.png | Bin 0 -> 1731 bytes .../scanbot_ic_crop_magnifier.png | Bin 0 -> 6202 bytes .../scanbot_ic_crop_side_handle.png | Bin 0 -> 551 bytes .../scanbot_camera_button_selector.xml | 20 + .../scanbot_camera_flash_btn_selector.xml | 18 + ...nbot_camera_toggle_text_color_selector.xml | 18 + .../drawable/scanbot_crop_button_selector.xml | 16 + .../drawable/scanbot_filter_apply_btn_bg.xml | 34 ++ ...nbot_filter_controls_seek_bar_progress.xml | 12 + ..._controls_seek_bar_progress_background.xml | 12 + ...scanbot_filter_controls_seek_bar_thumb.xml | 12 + ...canbot_filter_type_controller_selector.xml | 44 ++ ...bot_gallery_counter_rounded_background.xml | 16 + .../main/res/drawable/scanbot_ic_add_fab.xml | 16 + .../main/res/drawable/scanbot_ic_alert.xml | 16 + .../src/main/res/drawable/scanbot_ic_back.xml | 16 + .../drawable/scanbot_ic_black_and_white.xml | 25 + .../main/res/drawable/scanbot_ic_color.xml | 21 + .../src/main/res/drawable/scanbot_ic_crop.xml | 16 + .../main/res/drawable/scanbot_ic_delete.xml | 16 + .../res/drawable/scanbot_ic_dont_move.xml | 20 + .../src/main/res/drawable/scanbot_ic_edit.xml | 16 + .../main/res/drawable/scanbot_ic_filter.xml | 16 + .../drawable/scanbot_ic_filter_brightness.xml | 17 + .../scanbot_ic_filter_brightness_active.xml | 17 + .../scanbot_ic_filter_brightness_selector.xml | 12 + .../drawable/scanbot_ic_filter_contrast.xml | 17 + .../scanbot_ic_filter_contrast_active.xml | 17 + .../scanbot_ic_filter_contrast_selector.xml | 12 + .../res/drawable/scanbot_ic_filter_reset.xml | 17 + .../drawable/scanbot_ic_filter_sharpness.xml | 17 + .../scanbot_ic_filter_sharpness_active.xml | 17 + .../scanbot_ic_filter_sharpness_selector.xml | 12 + .../scanbot_ic_filter_type_black_white.xml | 23 + ...nbot_ic_filter_type_black_white_active.xml | 23 + ...ot_ic_filter_type_black_white_selector.xml | 12 + .../drawable/scanbot_ic_filter_type_color.xml | 25 + .../scanbot_ic_filter_type_color_active.xml | 25 + .../scanbot_ic_filter_type_color_selector.xml | 12 + .../scanbot_ic_filter_type_grayscale.xml | 24 + ...canbot_ic_filter_type_grayscale_active.xml | 24 + ...nbot_ic_filter_type_grayscale_selector.xml | 12 + .../scanbot_ic_filter_type_magic_color.xml | 26 + ...nbot_ic_filter_type_magic_color_active.xml | 26 + ...ot_ic_filter_type_magic_color_selector.xml | 12 + .../scanbot_ic_filter_type_magic_text.xml | 25 + ...anbot_ic_filter_type_magic_text_active.xml | 25 + ...bot_ic_filter_type_magic_text_selector.xml | 12 + .../drawable/scanbot_ic_filter_type_none.xml | 24 + .../scanbot_ic_filter_type_none_active.xml | 23 + .../scanbot_ic_filter_type_none_selector.xml | 12 + .../main/res/drawable/scanbot_ic_flash.xml | 20 + .../drawable/scanbot_ic_flash_selected.xml | 20 + .../res/drawable/scanbot_ic_grayscale.xml | 25 + .../res/drawable/scanbot_ic_magic_color.xml | 24 + .../res/drawable/scanbot_ic_magic_text.xml | 24 + .../main/res/drawable/scanbot_ic_no_file.xml | 24 + .../res/drawable/scanbot_ic_photo_camera.xml | 16 + .../res/drawable/scanbot_ic_poor_light.xml | 16 + .../res/drawable/scanbot_ic_rearrange.xml | 16 + .../main/res/drawable/scanbot_ic_rotate.xml | 16 + .../res/drawable/scanbot_ic_too_noisy.xml | 16 + .../main/res/drawable/scanbot_ic_touch.xml | 16 + .../main/res/drawable/scanbot_ic_zoom_in.xml | 19 + .../scanbot_user_guidance_background.xml | 12 + .../res/layout/scanbot_activity_camera.xml | 112 ++++ .../main/res/layout/scanbot_activity_crop.xml | 53 ++ .../res/layout/scanbot_activity_filter.xml | 54 ++ .../res/layout/scanbot_activity_gallery.xml | 112 ++++ .../res/layout/scanbot_activity_rearrange.xml | 76 +++ .../main/res/layout/scanbot_activity_save.xml | 156 +++++ ...scanbot_gallery_bottom_navigation_item.xml | 41 ++ .../layout/scanbot_import_progress_dialog.xml | 42 ++ .../res/layout/scanbot_item_rearrange.xml | 39 ++ .../layout/scanbot_lock_progress_dialog.xml | 32 ++ .../main/res/layout/scanbot_user_guidance.xml | 29 + .../layout/scanbot_view_common_toolbar.xml | 60 ++ .../src/main/res/layout/scanbot_view_crop.xml | 30 + .../layout/scanbot_view_filter_controls.xml | 190 +++++++ .../layout/scanbot_view_save_file_type.xml | 81 +++ scanbot/src/main/res/values-de/strings.xml | 58 ++ scanbot/src/main/res/values-es/strings.xml | 58 ++ scanbot/src/main/res/values-fr/strings.xml | 58 ++ scanbot/src/main/res/values-it/strings.xml | 58 ++ scanbot/src/main/res/values-night/theme.xml | 71 +++ scanbot/src/main/res/values-nl/strings.xml | 58 ++ scanbot/src/main/res/values/attrs.xml | 85 +++ .../src/main/res/values/colors-palette.xml | 26 + scanbot/src/main/res/values/dimens.xml | 58 ++ .../src/main/res/values/preference_keys.xml | 14 + scanbot/src/main/res/values/strings.xml | 58 ++ scanbot/src/main/res/values/styles.xml | 43 ++ scanbot/src/main/res/values/theme.xml | 71 +++ .../res/values/untranslatable_strings.xml | 14 + settings.gradle.kts | 6 +- 1002 files changed, 35202 insertions(+), 622 deletions(-) create mode 100644 app/src/brandIonos/assets/branding/config.json create mode 100644 app/src/brandIonos/res/drawable-night/nextcloud_logo.xml create mode 100644 app/src/brandIonos/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/brandIonos/res/drawable/ic_launcher_monochrome.xml create mode 100644 app/src/brandIonos/res/drawable/ionos_logo.xml create mode 100644 app/src/brandIonos/res/drawable/logo.xml create mode 100644 app/src/brandIonos/res/drawable/nextcloud_logo.xml create mode 100644 app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/brandIonos/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/brandIonos/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/brandIonos/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/brandIonos/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/brandIonos/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/brandIonos/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/brandIonos/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/brandIonos/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/brandIonos/res/values-de/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-en-rCA/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-en-rUS/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-es-rMX/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-es-rUS/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-es/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-fr-rCA/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-fr/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-it/ionos-setup.xml create mode 100644 app/src/brandIonos/res/values-night/ionos-colors.xml create mode 100644 app/src/brandIonos/res/values/ionos-colors.xml create mode 100644 app/src/brandIonos/res/values/ionos-setup.xml create mode 100644 app/src/brandStrato/assets/branding/config.json create mode 100644 app/src/brandStrato/res/color/radio_button_color.xml create mode 100644 app/src/brandStrato/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/brandStrato/res/drawable/ic_launcher_monochrome.xml create mode 100644 app/src/brandStrato/res/drawable/ionos_logo.xml create mode 100644 app/src/brandStrato/res/drawable/logo.xml create mode 100644 app/src/brandStrato/res/drawable/nextcloud_logo.xml create mode 100644 app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/brandStrato/res/values-night/strato-colors.xml create mode 100644 app/src/brandStrato/res/values/strato-colors-palette.xml create mode 100644 app/src/brandStrato/res/values/strato-colors.xml create mode 100644 app/src/brandStrato/res/values/strato-setup.xml create mode 100644 app/src/generic/java/com/ionos/analytics/FirebaseAnalyticsManager.kt create mode 100644 app/src/gplay/ic_launcher-playstore.png create mode 100644 app/src/gplay/java/com/ionos/analytics/FirebaseAnalyticsManager.kt create mode 100644 app/src/gplay/res/animator/progress_bar_login_indeterminate.xml create mode 100644 app/src/gplay/res/color-night/drawer_item_shape_color.xml create mode 100644 app/src/gplay/res/color/drawer_item_shape_color.xml create mode 100644 app/src/gplay/res/drawable-night/favorite.xml create mode 100644 app/src/gplay/res/drawable-night/file_image.xml create mode 100644 app/src/gplay/res/drawable-night/file_movie.xml create mode 100644 app/src/gplay/res/drawable-night/file_sound.xml create mode 100644 app/src/gplay/res/drawable-night/shared_via_link.xml create mode 100644 app/src/gplay/res/drawable-night/shared_via_users.xml create mode 100644 app/src/gplay/res/drawable/account_circle_white.xml create mode 100644 app/src/gplay/res/drawable/add_to_home_screen.xml create mode 100644 app/src/gplay/res/drawable/all_files.xml create mode 100644 app/src/gplay/res/drawable/bottom_sheet_drag_handle.xml create mode 100644 app/src/gplay/res/drawable/dialog_background.xml create mode 100644 app/src/gplay/res/drawable/dialog_item_background.xml create mode 100644 app/src/gplay/res/drawable/e2e_border.xml create mode 100644 app/src/gplay/res/drawable/exo_divider.xml create mode 100644 app/src/gplay/res/drawable/exo_ic_check.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_audiotrack.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_pause.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_play.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_settings.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_simple_fastforward.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_simple_rewind.xml create mode 100644 app/src/gplay/res/drawable/exo_styled_controls_speed.xml create mode 100644 app/src/gplay/res/drawable/exo_thumb.xml create mode 100644 app/src/gplay/res/drawable/favorite.xml create mode 100644 app/src/gplay/res/drawable/file.xml create mode 100644 app/src/gplay/res/drawable/file_calendar.xml create mode 100644 app/src/gplay/res/drawable/file_details_search_background.xml create mode 100644 app/src/gplay/res/drawable/file_image.xml create mode 100644 app/src/gplay/res/drawable/file_movie.xml create mode 100644 app/src/gplay/res/drawable/file_multiple.xml create mode 100644 app/src/gplay/res/drawable/file_sound.xml create mode 100644 app/src/gplay/res/drawable/filelist_action_icon_background.xml create mode 100644 app/src/gplay/res/drawable/folder.xml create mode 100644 app/src/gplay/res/drawable/folder_star_32dp.xml create mode 100644 app/src/gplay/res/drawable/grid_mode_item_background.xml create mode 100644 app/src/gplay/res/drawable/grid_mode_item_outline.xml create mode 100644 app/src/gplay/res/drawable/grid_mode_more_button_background.xml create mode 100644 app/src/gplay/res/drawable/grid_mode_selected_item_background.xml create mode 100644 app/src/gplay/res/drawable/ic_action_create_dir.xml create mode 100644 app/src/gplay/res/drawable/ic_action_upload.xml create mode 100644 app/src/gplay/res/drawable/ic_activity.xml create mode 100644 app/src/gplay/res/drawable/ic_alphabetical_asc.xml create mode 100644 app/src/gplay/res/drawable/ic_alphabetical_desc.xml create mode 100644 app/src/gplay/res/drawable/ic_arrow_back.xml create mode 100644 app/src/gplay/res/drawable/ic_assistant.xml create mode 100644 app/src/gplay/res/drawable/ic_camera.xml create mode 100644 app/src/gplay/res/drawable/ic_checkbox_blank_outline.xml create mode 100644 app/src/gplay/res/drawable/ic_checkbox_marked.xml create mode 100644 app/src/gplay/res/drawable/ic_close_search.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud_download.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud_sync.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud_sync_off.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud_sync_on.xml create mode 100644 app/src/gplay/res/drawable/ic_cloud_upload.xml create mode 100644 app/src/gplay/res/drawable/ic_contact_book.xml create mode 100644 app/src/gplay/res/drawable/ic_content_copy.xml create mode 100644 app/src/gplay/res/drawable/ic_delete.xml create mode 100644 app/src/gplay/res/drawable/ic_dots_vertical.xml create mode 100644 app/src/gplay/res/drawable/ic_edit.xml create mode 100644 app/src/gplay/res/drawable/ic_email.xml create mode 100644 app/src/gplay/res/drawable/ic_export.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_cancel_sync.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_edit.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_lock_file.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_move_or_copy.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_open_file_with.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_set_encrypted.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_stream_media.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_sync_file.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_unlock_file.xml create mode 100644 app/src/gplay/res/drawable/ic_file_action_unset_encrypted.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_account_group.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_external.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_key.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_link.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_lock.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_share.xml create mode 100644 app/src/gplay/res/drawable/ic_folder_overlay_upload.xml create mode 100644 app/src/gplay/res/drawable/ic_gallery_set_media_folder.xml create mode 100644 app/src/gplay/res/drawable/ic_gallery_show_photos.xml create mode 100644 app/src/gplay/res/drawable/ic_gallery_show_videos.xml create mode 100644 app/src/gplay/res/drawable/ic_gallery_tick.xml create mode 100644 app/src/gplay/res/drawable/ic_grid_mode_more.xml create mode 100644 app/src/gplay/res/drawable/ic_history.xml create mode 100644 app/src/gplay/res/drawable/ic_import.xml create mode 100644 app/src/gplay/res/drawable/ic_information_outline.xml create mode 100644 app/src/gplay/res/drawable/ic_link.xml create mode 100644 app/src/gplay/res/drawable/ic_list_empty_create_folder.xml create mode 100644 app/src/gplay/res/drawable/ic_list_empty_error.xml create mode 100644 app/src/gplay/res/drawable/ic_list_empty_folder.xml create mode 100644 app/src/gplay/res/drawable/ic_list_empty_recent.xml create mode 100644 app/src/gplay/res/drawable/ic_list_empty_shared.xml create mode 100644 app/src/gplay/res/drawable/ic_menu.xml create mode 100644 app/src/gplay/res/drawable/ic_modification_asc.xml create mode 100644 app/src/gplay/res/drawable/ic_modification_desc.xml create mode 100644 app/src/gplay/res/drawable/ic_pause.xml create mode 100644 app/src/gplay/res/drawable/ic_play.xml create mode 100644 app/src/gplay/res/drawable/ic_plus.xml create mode 100644 app/src/gplay/res/drawable/ic_progress_bar_login_indeterminate.xml create mode 100644 app/src/gplay/res/drawable/ic_rename.xml create mode 100644 app/src/gplay/res/drawable/ic_scan_document.xml create mode 100644 app/src/gplay/res/drawable/ic_search.xml create mode 100644 app/src/gplay/res/drawable/ic_search_grey.xml create mode 100644 app/src/gplay/res/drawable/ic_select_all.xml create mode 100644 app/src/gplay/res/drawable/ic_select_none.xml create mode 100644 app/src/gplay/res/drawable/ic_share.xml create mode 100644 app/src/gplay/res/drawable/ic_share_settings.xml create mode 100644 app/src/gplay/res/drawable/ic_size_asc.xml create mode 100644 app/src/gplay/res/drawable/ic_size_desc.xml create mode 100644 app/src/gplay/res/drawable/ic_star.xml create mode 100644 app/src/gplay/res/drawable/ic_star_outline.xml create mode 100644 app/src/gplay/res/drawable/ic_synced.xml create mode 100644 app/src/gplay/res/drawable/ic_tick.xml create mode 100644 app/src/gplay/res/drawable/ic_unshare.xml create mode 100644 app/src/gplay/res/drawable/ic_unshared.xml create mode 100644 app/src/gplay/res/drawable/ic_user.xml create mode 100644 app/src/gplay/res/drawable/ic_view_list.xml create mode 100644 app/src/gplay/res/drawable/ic_view_module.xml create mode 100644 app/src/gplay/res/drawable/ic_wallpaper.xml create mode 100644 app/src/gplay/res/drawable/image_32dp.xml create mode 100644 app/src/gplay/res/drawable/nav_favorites.xml create mode 100644 app/src/gplay/res/drawable/nav_notifications.xml create mode 100644 app/src/gplay/res/drawable/nav_on_device.xml create mode 100644 app/src/gplay/res/drawable/nav_photos.xml create mode 100644 app/src/gplay/res/drawable/nav_settings.xml create mode 100644 app/src/gplay/res/drawable/nav_shared.xml create mode 100644 app/src/gplay/res/drawable/nav_trashbin.xml create mode 100644 app/src/gplay/res/drawable/notification_icon.xml create mode 100644 app/src/gplay/res/drawable/notifications_icon.xml create mode 100644 app/src/gplay/res/drawable/player_ic_audio.xml create mode 100644 app/src/gplay/res/drawable/player_ic_pause.xml create mode 100644 app/src/gplay/res/drawable/player_ic_play.xml create mode 100644 app/src/gplay/res/drawable/player_ic_repeat.xml create mode 100644 app/src/gplay/res/drawable/player_ic_shuffle.xml create mode 100644 app/src/gplay/res/drawable/player_ic_skip_next.xml create mode 100644 app/src/gplay/res/drawable/player_ic_skip_previous.xml create mode 100644 app/src/gplay/res/drawable/player_ic_video.xml create mode 100644 app/src/gplay/res/drawable/progress_bar_login_indeterminate.xml create mode 100644 app/src/gplay/res/drawable/shared_via_link.xml create mode 100644 app/src/gplay/res/drawable/shared_via_users.xml create mode 100644 app/src/gplay/res/drawable/uploads.xml create mode 100644 app/src/gplay/res/drawable/video_32dp.xml create mode 100644 app/src/gplay/res/drawable/video_white.xml create mode 100644 app/src/gplay/res/layout/account_setup.xml create mode 100644 app/src/gplay/res/layout/account_setup_webview.xml create mode 100644 app/src/gplay/res/layout/activity_preview_media.xml create mode 100644 app/src/gplay/res/layout/activity_splash.xml create mode 100644 app/src/gplay/res/layout/backup_fragment.xml create mode 100644 app/src/gplay/res/layout/deep_link_login.xml create mode 100644 app/src/gplay/res/layout/drawer.xml create mode 100644 app/src/gplay/res/layout/empty_list_media.xml create mode 100644 app/src/gplay/res/layout/exo_player_control_view.xml create mode 100644 app/src/gplay/res/layout/exo_player_view.xml create mode 100644 app/src/gplay/res/layout/file_actions_bottom_sheet.xml create mode 100644 app/src/gplay/res/layout/file_actions_bottom_sheet_item.xml create mode 100644 app/src/gplay/res/layout/file_details_fragment.xml create mode 100644 app/src/gplay/res/layout/file_details_share_link_share_item.xml create mode 100644 app/src/gplay/res/layout/file_details_sharing_fragment.xml create mode 100644 app/src/gplay/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml create mode 100644 app/src/gplay/res/layout/fragment_gallery_bottom_sheet.xml create mode 100644 app/src/gplay/res/layout/grid_item.xml create mode 100644 app/src/gplay/res/layout/info_box.xml create mode 100644 app/src/gplay/res/layout/item_quick_share_permissions.xml create mode 100644 app/src/gplay/res/layout/list_item.xml create mode 100644 app/src/gplay/res/layout/login_flow_info_layout_v2.xml create mode 100644 app/src/gplay/res/layout/media_control.xml create mode 100644 app/src/gplay/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml create mode 100644 app/src/gplay/res/layout/settings_preference_first_group.xml create mode 100644 app/src/gplay/res/layout/settings_preference_group.xml create mode 100644 app/src/gplay/res/layout/settings_preference_list_item.xml create mode 100644 app/src/gplay/res/layout/settings_preference_switcher.xml create mode 100644 app/src/gplay/res/layout/settings_privacy_switchers.xml create mode 100644 app/src/gplay/res/layout/share_activity.xml create mode 100644 app/src/gplay/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml create mode 100644 app/src/gplay/res/layout/simple_spinner_item.xml create mode 100644 app/src/gplay/res/layout/sorting_order_fragment.xml create mode 100644 app/src/gplay/res/layout/synced_folders_item_header.xml create mode 100644 app/src/gplay/res/layout/toolbar_standard.xml create mode 100644 app/src/gplay/res/layout/view_authorization_method.xml create mode 100644 app/src/gplay/res/layout/view_login_background.xml create mode 100644 app/src/gplay/res/menu/partial_drawer_entries.xml create mode 100644 app/src/gplay/res/values-land/ionos-dimens.xml create mode 100644 app/src/gplay/res/values-large/ionos-dimens.xml create mode 100644 app/src/gplay/res/values-v27/ionos-styles.xml create mode 100644 app/src/gplay/res/values/ionos-dimens.xml create mode 100644 app/src/gplay/res/values/ionos-styles.xml create mode 100644 app/src/gplay/res/values/player-colors.xml delete mode 100644 app/src/gplay/res/values/setup.xml create mode 100644 app/src/gplay/res/xml/preferences.xml create mode 100644 app/src/huawei/java/com/ionos/analytics/FirebaseAnalyticsManager.kt create mode 100644 app/src/main/assets/branding/config.json create mode 100644 app/src/main/java/com/ionos/analytics/AnalyticsManager.kt create mode 100644 app/src/main/java/com/ionos/annotation/IonosCustomization.kt create mode 100644 app/src/main/java/com/ionos/authorization_method/AuthorizationMethodActivity.kt create mode 100644 app/src/main/java/com/ionos/branding/BrandConfig.kt create mode 100644 app/src/main/java/com/ionos/branding/BrandConfigProvider.kt create mode 100644 app/src/main/java/com/ionos/di/StratoModule.kt create mode 100644 app/src/main/java/com/ionos/player/PlayerModule.kt create mode 100644 app/src/main/java/com/ionos/player/media3/MediaNotificationProvider.kt create mode 100644 app/src/main/java/com/ionos/player/media3/PlaybackModelImpl.kt create mode 100644 app/src/main/java/com/ionos/player/media3/PlaybackModelPlayerListener.kt create mode 100644 app/src/main/java/com/ionos/player/media3/PlaybackService.kt create mode 100644 app/src/main/java/com/ionos/player/media3/PlaybackStateFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/common/MediaItemFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/common/MediaMetadata.kt create mode 100644 app/src/main/java/com/ionos/player/media3/common/PlayerFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/controller/MediaController.kt create mode 100644 app/src/main/java/com/ionos/player/media3/controller/MediaControllerFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/datasource/DataSourceFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/datasource/StreamDataSource.kt create mode 100644 app/src/main/java/com/ionos/player/media3/exoplayer/ExoPlayerFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/exoplayer/MediaSourceFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfig.kt create mode 100644 app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfigStore.kt create mode 100644 app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionPlayerListener.kt create mode 100644 app/src/main/java/com/ionos/player/media3/session/MediaSessionActivityFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/session/MediaSessionBitmapLoader.kt create mode 100644 app/src/main/java/com/ionos/player/media3/session/MediaSessionCallback.kt create mode 100644 app/src/main/java/com/ionos/player/media3/session/MediaSessionFactory.kt create mode 100644 app/src/main/java/com/ionos/player/media3/session/MediaSessionHolder.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFile.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFileMapper.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFileType.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFileUriMapper.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFiles.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFilesComparator.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackFilesRepository.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackModel.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackModelCompositeListener.kt create mode 100644 app/src/main/java/com/ionos/player/model/PlaybackSettings.kt create mode 100644 app/src/main/java/com/ionos/player/model/SourceException.kt create mode 100644 app/src/main/java/com/ionos/player/model/ThumbnailLoader.kt create mode 100644 app/src/main/java/com/ionos/player/model/VideoViewSetter.kt create mode 100644 app/src/main/java/com/ionos/player/model/error_strategy/DefaultPlaybackErrorStrategy.kt create mode 100644 app/src/main/java/com/ionos/player/model/error_strategy/PlaybackErrorStrategy.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/PlaybackItemMetadata.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/PlaybackItemState.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/PlaybackState.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/PlayerState.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/PlayerStateAnalyzer.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/RepeatMode.kt create mode 100644 app/src/main/java/com/ionos/player/model/state/VideoSize.kt create mode 100644 app/src/main/java/com/ionos/player/ui/PlayerActivity.kt create mode 100644 app/src/main/java/com/ionos/player/ui/PlayerLauncher.kt create mode 100644 app/src/main/java/com/ionos/player/ui/PlayerScreenEvent.kt create mode 100644 app/src/main/java/com/ionos/player/ui/PlayerView.kt create mode 100644 app/src/main/java/com/ionos/player/ui/PlayerViewModel.kt create mode 100644 app/src/main/java/com/ionos/player/ui/audio/AudioFileFragment.kt create mode 100644 app/src/main/java/com/ionos/player/ui/audio/AudioFileFragmentFactory.kt create mode 100644 app/src/main/java/com/ionos/player/ui/audio/AudioPlayerView.kt create mode 100644 app/src/main/java/com/ionos/player/ui/common/PlayerBasePresenter.kt create mode 100644 app/src/main/java/com/ionos/player/ui/common/PlayerProgressIndicator.kt create mode 100644 app/src/main/java/com/ionos/player/ui/control/PlayerControl.kt create mode 100644 app/src/main/java/com/ionos/player/ui/control/PlayerControlPresenter.kt create mode 100644 app/src/main/java/com/ionos/player/ui/control/PlayerControlView.java create mode 100644 app/src/main/java/com/ionos/player/ui/control/listener/MultipleClickListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewCompositeListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/PlayerPager.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/PlayerPagerFragmentFactory.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/PlayerPagerMode.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/adapter/AbstractFragmentPagerAdapter.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/adapter/DefaultFragmentPagerAdapter.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/adapter/InfiniteFragmentPagerAdapter.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/listener/NullPlayerPagerListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/listener/OnPageChangeCompositeListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/pager/listener/PlayerPagerListener.java create mode 100644 app/src/main/java/com/ionos/player/ui/video/VideoFileFragment.kt create mode 100644 app/src/main/java/com/ionos/player/ui/video/VideoFileFragmentFactory.kt create mode 100644 app/src/main/java/com/ionos/player/ui/video/VideoPlayerView.kt create mode 100644 app/src/main/java/com/ionos/player/util/Action.java create mode 100644 app/src/main/java/com/ionos/player/util/ContentResolver.kt create mode 100644 app/src/main/java/com/ionos/player/util/Context.kt create mode 100644 app/src/main/java/com/ionos/player/util/PeriodicAction.java create mode 100644 app/src/main/java/com/ionos/player/util/ScreenUtils.kt create mode 100644 app/src/main/java/com/ionos/player/util/SystemVersion.java create mode 100644 app/src/main/java/com/ionos/player/util/WindowWrapper.kt create mode 100644 app/src/main/java/com/ionos/privacy/DataProtectionActivity.kt create mode 100644 app/src/main/java/com/ionos/privacy/DataProtectionViewModel.kt create mode 100644 app/src/main/java/com/ionos/privacy/PrivacyPreferences.kt create mode 100644 app/src/main/java/com/ionos/privacy/PrivacySettingsActivity.kt create mode 100644 app/src/main/java/com/ionos/privacy/PrivacySettingsViewModel.kt create mode 100644 app/src/main/java/com/ionos/scanbot/di/NCScanbotModule.kt create mode 100644 app/src/main/java/com/ionos/scanbot/image_loading/ImageLoaderImpl.kt create mode 100644 app/src/main/java/com/ionos/scanbot/image_loading/ImageRequestBuilderImpl.kt create mode 100644 app/src/main/java/com/ionos/scanbot/image_loading/ObjectKey.java create mode 100644 app/src/main/java/com/ionos/scanbot/license/DownloadLicenseRemoteOperation.kt create mode 100644 app/src/main/java/com/ionos/scanbot/license/LicenseResponseTransformer.kt create mode 100644 app/src/main/java/com/ionos/scanbot/license/LoadScanbotLicenseImpl.kt create mode 100644 app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseDownloadWorker.kt create mode 100644 app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseJobFactory.kt create mode 100644 app/src/main/java/com/ionos/scanbot/logger/ScanbotLogger.kt create mode 100644 app/src/main/java/com/ionos/scanbot/ui/ScanbotCameraPermissionHandler.kt create mode 100644 app/src/main/java/com/ionos/scanbot/upload/ScanbotUploader.kt create mode 100644 app/src/main/java/com/ionos/scanbot/upload/SelectDirectoryContractImpl.kt create mode 100644 app/src/main/java/com/ionos/scanbot/utils/GetLocalFreeSpaceImpl.kt create mode 100644 app/src/main/java/com/ionos/startup/IonosInitializer.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosAndroidViewThemeUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosAndroidXViewThemeUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosBuildHelper.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosDialogViewThemeUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosFilesSpecificViewThemeUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/IonosMaterialViewThemeUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/context/ContextUtils.kt create mode 100644 app/src/main/java/com/ionos/utils/text/CharSequenceUtils.kt create mode 100644 app/src/main/java/com/owncloud/android/authentication/IonosLoginDomainResolver.java create mode 100644 app/src/main/java/com/owncloud/android/ui/RoundedCornersConstraintLayout.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/SquareFrameLayout.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/AnimatedDrawerListener.java create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/SingleStateDrawerArrowDrawable.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/OCFileListThumbnailLoader.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/SimpleOCFileListBottomSheetDialog.java create mode 100644 app/src/main/java/com/owncloud/android/ui/interfaces/CompletionCallback.kt create mode 100644 app/src/main/res/anim/fade_in.xml create mode 100644 app/src/main/res/anim/fade_out.xml create mode 100644 app/src/main/res/color-night/radio_button_color.xml create mode 100644 app/src/main/res/color/fab_background_tint.xml create mode 100644 app/src/main/res/color/filled_button_bg_color.xml create mode 100644 app/src/main/res/color/filled_button_text_color.xml create mode 100644 app/src/main/res/color/outlined_button_bg_color.xml create mode 100644 app/src/main/res/color/outlined_button_stroke_color.xml create mode 100644 app/src/main/res/color/outlined_button_text_color.xml create mode 100644 app/src/main/res/color/radio_button_color.xml create mode 100644 app/src/main/res/color/text_button_text_color.xml create mode 100644 app/src/main/res/drawable-v33/player_ic_notification_audio.xml create mode 100644 app/src/main/res/drawable-v33/player_ic_notification_video.xml create mode 100644 app/src/main/res/drawable/background_splash.xml create mode 100644 app/src/main/res/drawable/grid_mode_item_background.xml create mode 100644 app/src/main/res/drawable/grid_mode_selected_item_background.xml create mode 100644 app/src/main/res/drawable/ic_data_protection.xml create mode 100644 app/src/main/res/drawable/ic_file_action_cancel_sync.xml create mode 100644 app/src/main/res/drawable/ic_file_action_edit.xml create mode 100644 app/src/main/res/drawable/ic_file_action_lock_file.xml create mode 100644 app/src/main/res/drawable/ic_file_action_move_or_copy.xml create mode 100644 app/src/main/res/drawable/ic_file_action_open_file_with.xml create mode 100644 app/src/main/res/drawable/ic_file_action_set_encrypted.xml create mode 100644 app/src/main/res/drawable/ic_file_action_stream_media.xml create mode 100644 app/src/main/res/drawable/ic_file_action_sync_file.xml create mode 100644 app/src/main/res/drawable/ic_file_action_unlock_file.xml create mode 100644 app/src/main/res/drawable/ic_file_action_unset_encrypted.xml create mode 100644 app/src/main/res/drawable/ic_folder_all_share_types.xml create mode 100644 app/src/main/res/drawable/ic_shared_all_types.xml create mode 100644 app/src/main/res/drawable/player_ic_audio.xml create mode 100644 app/src/main/res/drawable/player_ic_close.xml create mode 100644 app/src/main/res/drawable/player_ic_notification_audio.xml create mode 100644 app/src/main/res/drawable/player_ic_notification_video.xml create mode 100644 app/src/main/res/drawable/player_ic_pause.xml create mode 100644 app/src/main/res/drawable/player_ic_play.xml create mode 100644 app/src/main/res/drawable/player_ic_repeat.xml create mode 100644 app/src/main/res/drawable/player_ic_shuffle.xml create mode 100644 app/src/main/res/drawable/player_ic_skip_next.xml create mode 100644 app/src/main/res/drawable/player_ic_skip_previous.xml create mode 100644 app/src/main/res/drawable/player_ic_video.xml create mode 100644 app/src/main/res/drawable/player_progress_drawable.xml create mode 100644 app/src/main/res/drawable/player_progress_thumb.xml create mode 100644 app/src/main/res/drawable/rounded_rect_4dp.xml create mode 100644 app/src/main/res/layout/activity_data_protection.xml create mode 100644 app/src/main/res/layout/activity_data_protection_detail_page.xml create mode 100644 app/src/main/res/layout/activity_data_protection_overview_page.xml create mode 100644 app/src/main/res/layout/activity_privacy_settings.xml create mode 100644 app/src/main/res/layout/ionos_item_media.xml create mode 100644 app/src/main/res/layout/player_audio_file_fragment.xml create mode 100644 app/src/main/res/layout/player_audio_view.xml create mode 100644 app/src/main/res/layout/player_control_view.xml create mode 100644 app/src/main/res/layout/player_pager.xml create mode 100644 app/src/main/res/layout/player_video_file_fragment.xml create mode 100644 app/src/main/res/layout/player_video_view.xml create mode 100644 app/src/main/res/layout/settings_privacy_switchers.xml create mode 100644 app/src/main/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml create mode 100644 app/src/main/res/layout/simple_spinner_item.xml create mode 100644 app/src/main/res/layout/view_authorization_method.xml create mode 100644 app/src/main/res/values-de/ionos-strings.xml create mode 100644 app/src/main/res/values-es-rCR/strings.xml create mode 100644 app/src/main/res/values-es-rGT/strings.xml create mode 100644 app/src/main/res/values-es-rSV/strings.xml create mode 100644 app/src/main/res/values-es/ionos-strings.xml create mode 100644 app/src/main/res/values-fr/ionos-strings.xml create mode 100644 app/src/main/res/values-it/ionos-strings.xml create mode 100644 app/src/main/res/values-land/ionos-integers.xml create mode 100644 app/src/main/res/values-land/player-dimens.xml create mode 100644 app/src/main/res/values-large-land/ionos-integers.xml create mode 100644 app/src/main/res/values-large-land/player-dimens.xml create mode 100644 app/src/main/res/values-large/ionos-dimens.xml create mode 100644 app/src/main/res/values-large/ionos-integers.xml create mode 100644 app/src/main/res/values-large/player-dimens.xml create mode 100644 app/src/main/res/values-night/ionos-colors.xml create mode 100644 app/src/main/res/values-night/ionos-scanbot-theme.xml create mode 100644 app/src/main/res/values-nl/ionos-strings.xml create mode 100644 app/src/main/res/values/ionos-attrs.xml create mode 100644 app/src/main/res/values/ionos-bools.xml create mode 100644 app/src/main/res/values/ionos-colors-palette.xml create mode 100644 app/src/main/res/values/ionos-colors.xml create mode 100644 app/src/main/res/values/ionos-dimens.xml create mode 100644 app/src/main/res/values/ionos-integers.xml create mode 100644 app/src/main/res/values/ionos-scanbot-theme.xml create mode 100644 app/src/main/res/values/ionos-strings.xml create mode 100644 app/src/main/res/values/ionos-styles.xml create mode 100644 app/src/main/res/values/ionos-themes.xml create mode 100644 app/src/main/res/values/player-colors-palette.xml create mode 100644 app/src/main/res/values/player-colors.xml create mode 100644 app/src/main/res/values/player-dimens.xml create mode 100644 app/src/main/res/values/player-strings.xml create mode 100644 app/src/main/res/values/player-styles.xml create mode 100644 app/src/qa/java/com/ionos/analytics/FirebaseAnalyticsManager.kt create mode 100644 app/src/test/java/com/ionos/player/model/error_strategy/DefaultPlaybackErrorStrategyTest.java create mode 100644 app/src/test/java/com/ionos/player/ui/pager/PlayerPagerTest.java create mode 100644 app/src/test/java/com/owncloud/android/authentication/IonosLoginDomainResolverTest.java create mode 100644 app/src/versionDev/java/com/ionos/analytics/FirebaseAnalyticsManager.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300053.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300053.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300151.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300290.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300290.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300390.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300390.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300490.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300490.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300590.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300590.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300690.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300690.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30300790.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30300790.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310052.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310052.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310151.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310251.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310251.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310290.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310290.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310351.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310351.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310390.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310390.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30310490.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30310490.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320290.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320290.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320351.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320351.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320352.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320352.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320353.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320353.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30320390.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30320390.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30340051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30340051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30340052.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30340052.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30340090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30340090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30340151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30340151.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30340190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30340190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350052.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350052.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350053.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350053.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350054.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350054.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30350290.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30350290.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30360051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30360051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/30360052.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/30360052.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330000090.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330000090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330000151.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330000151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330000190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330000190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010052.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010052.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010053.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010053.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010151.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010152.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010152.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010190.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/330010290.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/330010290.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/340000051.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/340000051.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/340000090.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/340000090.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/340000151.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/340000151.txt.license create mode 100644 fastlane/metadata/android/en-US/changelogs/340000190.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/340000190.txt.license create mode 100644 scanbot/.gitignore create mode 100644 scanbot/build.gradle.kts create mode 100644 scanbot/jacoco.exec create mode 100644 scanbot/proguard-rules.pro create mode 100644 scanbot/src/main/AndroidManifest.xml create mode 100644 scanbot/src/main/assets/ocr_blobs/eng.traineddata create mode 100644 scanbot/src/main/assets/ocr_blobs/fra.traineddata create mode 100644 scanbot/src/main/assets/ocr_blobs/osd.traineddata create mode 100644 scanbot/src/main/java/com/ionos/scanbot/availability/Availability.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/availability/ScanbotFeatureAvailability.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/availability/ScanbotLicenseAvailability.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/cleaner/TempFilesCleaner.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/controller/ScanbotController.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/controller/ScanbotControllerImpl.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/controller/StateManager.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/ScanbotModule.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/ScanbotTracking.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/qualifiers/Scanbot.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/qualifiers/ScanbotLicense.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/qualifiers/ScanbotLicenseKey.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/di/qualifiers/ScanbotLicenseKeyUrl.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/entity/ImageFormat.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/entity/ModifiedPicture.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/entity/OriginalPicture.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/entity/Picture.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/entity/SelectedContour.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/CreateIntentException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/DecodeBitmapException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/ImportPictureException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/InvalidFileNameException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/NoCameraPermissionException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/NoFreeLocalSpaceException.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/OpenFileException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/PictureNotFoundException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/ReadPictureBitmapException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/RecognizeDocumentException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/RenderDocumentException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/exception/SaveDocumentException.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/Filter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/FilterType.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/Filterable.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/color/ColorFilter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/color/ColorFilterType.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/crop/CropFilter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/filter/rotate/RotateFilter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/image_loader/ImageLoader.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/image_loader/ImageLoaderOptions.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/image_loader/ImageRequestBuilder.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/image_loader/ScaleType.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/initializer/ScanbotInitializer.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/initializer/ScanbotInitializerImpl.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/initializer/TryToInitScanbotSdk.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/KeyStore.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/LicenseKeyStore.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/LoadScanbotLicense.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/PreferencesKeyStore.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/algorithm/AesGcmEncryptionAlgorithm.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/algorithm/AlgorithmParameterSpecFactory.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/algorithm/AlgorithmParameterSpecFactoryImpl.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/algorithm/EncryptionAlgorithm.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/key/AesGcmKey.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/key/AesGcmKeyGenerator.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/security/key/AesGcmKeyRepository.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/settings/SecureEditor.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/settings/SecureEncryptor.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/license/oath/settings/SecureSharedPreferences.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/network/NetworkInfoIsAbsentException.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/network/NetworkStateBundle.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/network/NetworkStateChangeReceiver.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/network/NetworkUtils.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/ContourDetectorParamsProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/ContourDetectorProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/DocumentNameProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/FileProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/SdkProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/provider/SupportedLanguagesProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/repository/PictureRepository.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/repository/RepositoryFacade.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/repository/bitmap/BitmapDecoder.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/repository/bitmap/BitmapRepository.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/repository/bitmap/BitmapSaver.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/base/BaseActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/base/BaseScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/base/BaseViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/base/BaseWindowInsetsActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/CameraActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/CameraScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/CameraViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/CameraViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/image_picker/ImagePickerContract.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/image_picker/ImagePickerLauncher.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/ui_components/CameraViewController.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/ui_components/ImportProgressDialog.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/ui_components/NoFreeSpaceMessageDialogWrapper.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/ui_components/UserGuidanceView.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/AddPictureToRepository.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/ExtractPictureTransformation.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/GetCameraScreenErrorMessage.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/GetUserGuidanceStatus.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/ReadContentToByteArray.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/UserGuidanceStatus.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/import_pictures/ImportPictures.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/import_pictures/ImportPicturesInputItem.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/camera/use_case/import_pictures/ImportPicturesState.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/common/ExitDialog.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/common/LockProgressDialog.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/common/use_case/SelectContour.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/common/use_case/open_screen/OpenScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/common/use_case/open_screen/OpenScreenIntent.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/crop/CropActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/crop/CropScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/crop/CropViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/crop/CropViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/crop/view/CropView.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/FilterActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/FilterScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/FilterViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/FilterViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/use_case/GetColorFilterName.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/view/FilterControls.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/filter/view/FilterControlsListener.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/GalleryActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/GalleryBottomNavigationItem.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/GalleryScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/GalleryViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/GalleryViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/pager/GalleryPagerAdapter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/pager/GalleryPagerItem.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/gallery/use_case/GetColorFilterIcon.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/RearrangeActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/RearrangeScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/RearrangeViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/RearrangeViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/recycler/RearrangeAdapter.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/recycler/RearrangeCallback.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/recycler/RearrangeDiffCallback.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/recycler/RearrangeItem.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/rearrange/recycler/RearrangeViewHolder.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/SaveActivity.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/SaveScreen.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/SaveViewModel.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/SaveViewModelFactory.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/SelectDirectoryContract.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/ValidateFilesForUpload.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/ValidateFilesForUploadSynchronous.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/name/GetFileNames.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/name/GetImageFileNames.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/name/GetPdfFileName.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/save/SaveDocument.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/save/SaveImages.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/use_case/save/SavePdf.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/screens/save/view/FileTypeView.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotCameraScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotCropScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotFilterScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotGalleryScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotRearrangeScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotSaveScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/ScanbotScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotCameraScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotCropScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotFilterScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotGalleryScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotRearrangeScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/tracking/stub/StubScanbotSaveScreenEventTracker.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/target_provider/UploadTarget.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/target_provider/UploadTargetProvider.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/target_provider/UploadTargetRepository.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/target_provider/UploadTargetRepositoryImpl.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/use_case/StartUpload.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/upload/use_case/Uploader.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/FileUtils.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/GetLocalFreeSpace.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/collections/OrderedHashMap.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/config/Configuration.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/context/ContextUtils.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/graphics/Bitmap.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/graphics/ExifInterface.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/graphics/Matrix.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/gson/RuntimeTypeAdapterFactory.java create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/io/File.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/io/SaveErrorOutputStream.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/lifedata/MutableStateLiveData.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/lifedata/StateLiveData.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/logger/Logger.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/logger/LoggerUtil.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/permission/PermissionUtils.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/rx/Rx.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/widget/DiffUtilsUpdateCallback.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/widget/ViewPager.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/window/WindowInsetsUtils.kt create mode 100644 scanbot/src/main/java/com/ionos/scanbot/util/window/WindowWrapper.kt create mode 100644 scanbot/src/main/res/color/scanbot_file_type_text_color.xml create mode 100644 scanbot/src/main/res/drawable-xxhdpi/scanbot_ic_crop_corner_handle.png create mode 100644 scanbot/src/main/res/drawable-xxhdpi/scanbot_ic_crop_magnifier.png create mode 100644 scanbot/src/main/res/drawable-xxhdpi/scanbot_ic_crop_side_handle.png create mode 100644 scanbot/src/main/res/drawable/scanbot_camera_button_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_camera_flash_btn_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_camera_toggle_text_color_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_crop_button_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_filter_apply_btn_bg.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_filter_controls_seek_bar_progress.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_filter_controls_seek_bar_progress_background.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_filter_controls_seek_bar_thumb.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_filter_type_controller_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_gallery_counter_rounded_background.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_add_fab.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_alert.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_back.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_black_and_white.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_color.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_crop.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_delete.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_dont_move.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_edit.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_brightness.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_brightness_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_brightness_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_contrast.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_contrast_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_contrast_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_reset.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_sharpness.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_sharpness_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_sharpness_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_black_white.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_black_white_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_black_white_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_color.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_color_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_color_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_grayscale.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_grayscale_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_grayscale_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_color.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_color_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_color_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_text.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_text_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_magic_text_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_none.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_none_active.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_filter_type_none_selector.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_flash.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_flash_selected.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_grayscale.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_magic_color.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_magic_text.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_no_file.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_photo_camera.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_poor_light.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_rearrange.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_rotate.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_too_noisy.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_touch.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_ic_zoom_in.xml create mode 100644 scanbot/src/main/res/drawable/scanbot_user_guidance_background.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_camera.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_crop.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_filter.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_gallery.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_rearrange.xml create mode 100644 scanbot/src/main/res/layout/scanbot_activity_save.xml create mode 100644 scanbot/src/main/res/layout/scanbot_gallery_bottom_navigation_item.xml create mode 100644 scanbot/src/main/res/layout/scanbot_import_progress_dialog.xml create mode 100644 scanbot/src/main/res/layout/scanbot_item_rearrange.xml create mode 100644 scanbot/src/main/res/layout/scanbot_lock_progress_dialog.xml create mode 100644 scanbot/src/main/res/layout/scanbot_user_guidance.xml create mode 100644 scanbot/src/main/res/layout/scanbot_view_common_toolbar.xml create mode 100644 scanbot/src/main/res/layout/scanbot_view_crop.xml create mode 100644 scanbot/src/main/res/layout/scanbot_view_filter_controls.xml create mode 100644 scanbot/src/main/res/layout/scanbot_view_save_file_type.xml create mode 100644 scanbot/src/main/res/values-de/strings.xml create mode 100644 scanbot/src/main/res/values-es/strings.xml create mode 100644 scanbot/src/main/res/values-fr/strings.xml create mode 100644 scanbot/src/main/res/values-it/strings.xml create mode 100644 scanbot/src/main/res/values-night/theme.xml create mode 100644 scanbot/src/main/res/values-nl/strings.xml create mode 100644 scanbot/src/main/res/values/attrs.xml create mode 100644 scanbot/src/main/res/values/colors-palette.xml create mode 100644 scanbot/src/main/res/values/dimens.xml create mode 100644 scanbot/src/main/res/values/preference_keys.xml create mode 100644 scanbot/src/main/res/values/strings.xml create mode 100644 scanbot/src/main/res/values/styles.xml create mode 100644 scanbot/src/main/res/values/theme.xml create mode 100644 scanbot/src/main/res/values/untranslatable_strings.xml diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index bc106e52608a..2e31899f7ffd 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9211702adf0b..8419828be50f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,9 +23,11 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.spotless) + alias(libs.plugins.kapt) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.spotbugs) alias(libs.plugins.detekt) // needed to make renovate run without shot, as shot requires Android SDK @@ -149,9 +151,23 @@ android { dimension = "default" } + // IONOS HiDrive Next brand. Shared white-label base lives in src/gplay, + // Payload in src/brandIonos register("gplay") { - applicationId = "com.nextcloud.client" + applicationId = "com.ionos.hidrivenext" + dimension = "default" + versionCode = 18 + isDefault = true + resConfigs("en", "de", "es", "fr", "it", "en-rUS", "es-rUS", "en-rCA", "fr-rCA", "es-rMX") + } + + // HiDrive Next brand + // Payload in src/brandStrato + register("hidriveNext") { + applicationId = "com.strato.hidrivenext" dimension = "default" + versionCode = 1 + resConfigs("en", "de", "es", "fr", "it") } register("huawei") { @@ -233,14 +249,29 @@ android { getByName("androidTest") { assets.srcDirs(files("$projectDir/schemas")) } + + // src/gplay holds the shared white-label base + getByName("gplay") { + res.srcDir("src/brandIonos/res") + assets.srcDir("src/brandIonos/assets") + } + getByName("hidriveNext") { + manifest.srcFile("src/gplay/AndroidManifest.xml") + java.srcDir("src/gplay/java") + res.srcDir("src/gplay/res") + res.srcDir("src/brandStrato/res") + assets.srcDir("src/brandStrato/assets") + } + getByName("androidTestHidriveNext") { + java.srcDir("src/androidTestGplay/java") + } } } -ksp.arg("room.schemaLocation", "$projectDir/schemas") +kapt.useBuildCache = true -// Configure KSP for test variants -ksp.arg("dagger.moduleName", project.name) +ksp.arg("room.schemaLocation", "$projectDir/schemas") kotlin.compilerOptions.jvmTarget.set(JvmTarget.JVM_21) @@ -430,9 +461,9 @@ dependencies { // region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries // To enable the feature for another variant, add it here. - "gplayImplementation"(project(":appscan")) "huaweiImplementation"(project(":appscan")) "qaImplementation"(project(":appscan")) + "implementation"(project(":scanbot")) // endregion // region SpotBugs @@ -444,9 +475,8 @@ dependencies { implementation(libs.dagger) implementation(libs.dagger.android) implementation(libs.dagger.android.support) - ksp(libs.dagger.compiler) - ksp(libs.dagger.processor) - kspAndroidTest(libs.dagger.compiler) + kapt(libs.dagger.compiler) + kapt(libs.dagger.processor) // endregion // region Crypto @@ -510,6 +540,7 @@ dependencies { // region Google Play dependencies, upon each update first test: new registration, receive push "gplayImplementation"(libs.bundles.gplay) + "hidriveNextImplementation"(libs.bundles.gplay) // endregion // region common @@ -523,4 +554,9 @@ dependencies { // kotlinx.serialization implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.guava) + + // reactivex + implementation(libs.rxjava) + implementation(libs.rxandroid) } diff --git a/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt b/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt index 3ac80ebe9d6c..6610a19aab2a 100644 --- a/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt +++ b/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt @@ -7,41 +7,64 @@ package com.nextcloud.client.di +import com.ionos.scanbot.availability.Availability +import com.ionos.scanbot.di.qualifiers.Scanbot +import com.ionos.scanbot.di.qualifiers.ScanbotLicense import com.nextcloud.client.documentscan.AppScanOptionalFeature +import dagger.BindsInstance import dagger.Component -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test -/** - * Unit test for [VariantModule] that tests the reflection-based approach - * to conditionally load the ScanPageContract. - */ class VariantModuleTest { - private lateinit var component: TestVariantComponent + @Test + fun testAppScanWhenScanbotFeatureAndLicenseAvailableShouldBeAvailable() { + val feature = createAppScanOptionalFeature(isFeatureAvailable = true, isLicenseAvailable = true) - @Before - fun setup() { - component = DaggerVariantModuleTest_TestVariantComponent.create() + assertTrue(feature.isAvailable) } @Test - fun testAppScanWhenAvailableShouldReturnContract() { - val feature = component.appScanOptionalFeature() + fun testAppScanWhenScanbotFeatureNotAvailableShouldNotBeAvailable() { + val feature = createAppScanOptionalFeature(isFeatureAvailable = false, isLicenseAvailable = true) - assertTrue(feature.isAvailable) - assertNotEquals(AppScanOptionalFeature.Stub, feature) + assertFalse(feature.isAvailable) + } + + @Test + fun testAppScanWhenScanbotLicenseNotAvailableShouldNotBeAvailable() { + val feature = createAppScanOptionalFeature(isFeatureAvailable = true, isLicenseAvailable = false) - assertNotNull(feature.getScanContract()) + assertFalse(feature.isAvailable) } + @Test + fun testAppScanShouldNotProvideScanContract() { + val feature = createAppScanOptionalFeature(isFeatureAvailable = true, isLicenseAvailable = true) + + assertThrows(UnsupportedOperationException::class.java) { feature.getScanContract() } + } + + private fun createAppScanOptionalFeature( + isFeatureAvailable: Boolean, + isLicenseAvailable: Boolean + ): AppScanOptionalFeature = DaggerVariantModuleTest_TestVariantComponent.factory() + .create(Availability { isFeatureAvailable }, Availability { isLicenseAvailable }) + .appScanOptionalFeature() + @Component(modules = [VariantModule::class]) interface TestVariantComponent { fun appScanOptionalFeature(): AppScanOptionalFeature + + @Component.Factory + interface Factory { + fun create( + @BindsInstance @Scanbot featureAvailability: Availability, + @BindsInstance @ScanbotLicense licenseAvailability: Availability + ): TestVariantComponent + } } } diff --git a/app/src/brandIonos/assets/branding/config.json b/app/src/brandIonos/assets/branding/config.json new file mode 100644 index 000000000000..1d96de63113b --- /dev/null +++ b/app/src/brandIonos/assets/branding/config.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "loginUrls": { + "byCountry": { + "DE": "https://storage.ionos.de/index.php/login/v2", + "GB": "https://storage.ionos.co.uk/index.php/login/v2", + "ES": "https://storage.ionos.es/index.php/login/v2", + "FR": "https://storage.ionos.fr/index.php/login/v2", + "IT": "https://storage.ionos.it/index.php/login/v2", + "US": "https://storage.ionos.com/index.php/login/v2", + "CA": "https://storage.ionos.ca/index.php/login/v2", + "MX": "https://storage.ionos.mx/index.php/login/v2" + }, + "byLanguage": { + "de": "https://storage.ionos.de/index.php/login/v2", + "en": "https://storage.ionos.co.uk/index.php/login/v2", + "es": "https://storage.ionos.es/index.php/login/v2", + "fr": "https://storage.ionos.fr/index.php/login/v2", + "it": "https://storage.ionos.it/index.php/login/v2" + }, + "default": "https://storage.ionos.co.uk/index.php/login/v2" + } +} diff --git a/app/src/brandIonos/res/drawable-night/nextcloud_logo.xml b/app/src/brandIonos/res/drawable-night/nextcloud_logo.xml new file mode 100644 index 000000000000..939c59c3e3a2 --- /dev/null +++ b/app/src/brandIonos/res/drawable-night/nextcloud_logo.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandIonos/res/drawable/ic_launcher_foreground.xml b/app/src/brandIonos/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000000..8e79221f38d8 --- /dev/null +++ b/app/src/brandIonos/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/app/src/brandIonos/res/drawable/ic_launcher_monochrome.xml b/app/src/brandIonos/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000000..f58255035bb2 --- /dev/null +++ b/app/src/brandIonos/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/brandIonos/res/drawable/ionos_logo.xml b/app/src/brandIonos/res/drawable/ionos_logo.xml new file mode 100644 index 000000000000..9f971f82dba3 --- /dev/null +++ b/app/src/brandIonos/res/drawable/ionos_logo.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandIonos/res/drawable/logo.xml b/app/src/brandIonos/res/drawable/logo.xml new file mode 100644 index 000000000000..9f971f82dba3 --- /dev/null +++ b/app/src/brandIonos/res/drawable/logo.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandIonos/res/drawable/nextcloud_logo.xml b/app/src/brandIonos/res/drawable/nextcloud_logo.xml new file mode 100644 index 000000000000..41f4c5d33874 --- /dev/null +++ b/app/src/brandIonos/res/drawable/nextcloud_logo.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..1598d1379c11 --- /dev/null +++ b/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000000..448b38f276ae --- /dev/null +++ b/app/src/brandIonos/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/brandIonos/res/mipmap-hdpi/ic_launcher.webp b/app/src/brandIonos/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..3924cf7e16a7024c6cf1bed21359587a7ef56787 GIT binary patch literal 1132 zcmV-y1e5zxNk&Fw1ONb6MM6+kP&iCi1ONapN5ByfzXzeVZPm(J`+UdgUy4BhFW?Rv zDFHE(Y}HEdm5)mZI1IcKLG`Yl@l>@Bo7fl?0+Mfqzqjb&G>TsA1(gn>11a@U;u{9WE z!pMe9Wx@s`gACYoda`lo?DPybN76);$mn+W+TFu99yhJ}`1?iSyZ*K6?)Am_#idpi zsA^T=?Q+%+ZsK|G=Z$LpLlxS82RQWo_q6%;Q`R346jL(p7oTH>$H3`dAqxP&!fx(r z_pP&S+o)~3v$xK+y>+(n+O)H6+g{z-+jo)VlDy$Fxr^w(1GkYB$=N+-Z0!@I|L^1+ zCdmr?7L4Lb9*1M6_8mJ=jjO^Dzi{!XofCJV9~U1ni!6z@PR>l?ZHy!tB}eMQon4qD z-u{qskmSzHfTWduDDqpXz>zcomon6inuK+Dr%T`3jk?lcYI^R(I$KWXa0_*C zQ!Eu>11Be?;G8L^Q&oqXdj#ApZ*uO#R-`C6x2*Z0EK#}M-xp9~bNWpqUqySg7h z^&>V&53o{CmeQ$+WglcNsqp0r))U2b$gV+WE+jPo;GM**s(K@hj=2i10iBCF^JZ$H z1*GO99X@|Sh|5j&gnq;r1O( zyZcy(C)0+6dr|&>$+&k3*Q+2_^T3;XxHXjBe3E`2f?M@ zS$S8}Bo{?Cw)~vu%p~orwXrOr`e|1E%ZEG$z5iCt?3aYDl9hdpT6JsJ{77DlYFi853N&o;nt{|fT literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/brandIonos/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c70714af43c9540d8f8ebb402ca994e69b7c1a3c GIT binary patch literal 2706 zcmV;D3T^dLNk&GB3IG6CMM6+kP&iC}3IG5vN5ByfHHU(>Z6t?3?Cl;15itSXvzV3r zPfHU;N4eRyEqgMMySsaEcXxMpcXxO9iq1Qz+@bc)Y(bD+@82}q0QZoZ(%D=>!!b|GpUx183%NULY05p^S{~P9I+qP%p zVRf=^HAs@=D3VnT(}{Uzd6-#JN~!HXBe!iFNy*G?Jx%=r6>IxfyUx#qnK=h?HvIpI z>jXOgS)G$?I6D*g$xeY}NAO=!wM>e+yQK45>*{WOTvS1|ie(*h)sIwPps zg&Ay{36#+>^9ZuG>iUw()n=7(Q^ zSlhN~XUG1wjmLlAy+<#0Qb77i=@2AIHf`CqZQHhO+qUihw_w}0wFTRL*1hAvZ6rmi zbPvz$;m{K>TL92Y;GdxnQerF1X~e*DrWCQKQ9G`3lSu3oxxx}R+DQmn{&PeJ3WESOJ<9V& zCy7L~r?APMkq~tJnivFGYOznxjL59ayl(Bb)s;|?uOhjw4LTAK_nvK z7?BXnDHKO+y6&Y*A9v7*nSvVPVar0ibID$J)|ix}w4`UFGQiXvZlrm7QO)^e1@ zLfsImG*r(s{nD*HMSr3@eYeePDGd6?no9z?g5XZ$IE7kkmE02rD0&{Y3QG|Bwi zQrwfkWw_gu14j8O;q<43I|aZZr9@!H#-x{KEnnY_W6_Jlf&Gvx^W9M|9td?Zd<|Yy zJzN7VZj=EwT}u%zc{h;-&{Fl_We?Vuz~0udC?yh$@c?;v?Kwbm|51A%NnsFRyzbA#6WcIZTqm3^X`dPPb!7NbFL&IIr(KnupSPIJ{>wT(L!-Hl+J988r;0pZeNkK&;F7$E;HR;b@m$tW`;3l&JoYMQtY3Q zfvLCRTz$Ug-dg)?e*L=fTgSM(;?PY_UJ-Prj@F&YjHUyXp<93p%?aB)Pc<+AK#y~{ z_}!?8H3ZI{8SCHK+i&Q6dyFw{^()ZN;49USo*YVv0Jxd>(ryrMRLvV5pt`#OAdry+ zz^NM_iKwrF_Gm{_TYh_J3j2R5-8ldrDbWMKu0ww+MP+mdVKXOG9JVbiZXseSluXc}Y5M)=5hpkwRHSo^dp zJG9R>P6A*qe>K&0*AMfBA<;{*oTDh;wXv1+*vcg>ZAyEOF-KjjKDHI7oraz6bHzqz zm2ZrU3#T?=E~(+a%3d|SrzG~${S~onqxxacatCMyrLumSvHLO7juQSH!Hc^a^b>6~ zxy?eue&Eoib>Zb(E-|JC>Yk2~!qN-8n8{30(ww07oCY15$G!k7RsdqD4BOUQQZpn~ z^drS^Vd(;1{Oxk0q=x{+k3@s=!_D(Nn3(|WDXc21>&NN4ACwWo(iOaPeqFauqK5&` zj7A;>AkkIwc^jt?Q+@4#FleTls~HW!+A6#-ptrW+Q(fTGXZ8s6P8%QcOC68efz!p7asmoXLF`*1TE9D zb)-GU>=sAz`2ZA>HDC90!V+`%uB{!S87cgE`cGZOy?Bkb0VvLwXigpgf5H1)SIOd9 z*f2{Z6^xV7I80m`R3s&fyD4~|8D8@?hO)Koa{v&n*;}4!1`I_ulKX}VOE3Sa<~*W= z!&VIq}-q6mKgsfg`d;sKHmr1r7 zgiua6MvlUkb)0`))mG(~$Z^U615Mi~Ny+dpa?W`w3ryqIPaUY-D*{sxc~Nmx1Odp` zA5EWB9&VTka2evvdG;AUSq?XKN6mllYcp=wP6Z$foEgV!yrDS*a_#*Wv(FDxX!? zSlusa9>Ax__AqWlV#iqBFIPS*Zp++qmc%Rpz;rDD8339MM=w56lKsiY(KG1+%kHCY z^bGlv=QbSu#OK$z766kIw^nBXH=+Xobn32dRL2-iq}t+5HwK^R%qVgVMA4ZIeWBfdq;D+G$$DIxfIBVKX%;8@fxqa{@3#Z zz_s}KgXkE8^UJOq(Zc|!PAdMtvaUY|N$u0BGgjm2M3*h5MG z^2*OUbd1Z+MH36(03fs6{6Tb-DF9dx7<%9%0BSP|fPMimo+$gTxKutX?l-l4 z&A8&D2ZjzyRu M9{JSVDH5{-0VWhNJ^%m! literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-mdpi/ic_launcher.webp b/app/src/brandIonos/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..d443a40101c5a9353f6f3cad090940ff33cc05b6 GIT binary patch literal 878 zcmV-!1CjhvNk&Fy0{{S5MM6+kP&iCk0{{RoFTe{BF9xBJBu7&A-hwzdo<9=r?`Ed* z4GCD0Y`dy@ACdud;Tb0g;4i#*_Fiyf+jiCL%f~~;kf{R|q5j*jsrT>Qvtik`Wk}i) zQ8wn_>2a?5H`>go0{{YupbP?tAc5iNJ`kjp29ZJkR^Jo7@_bJHCma~K)qbL1Kg^dh zt(C)l!x4cI1A%}g!KlBG4g&(BGzg?@Tps^E@0KkCHz+wUP|zg}M+VsDoxXmlXo96(^``g_+%2c3ADYZ($v=&t;rK&8~4TrRA+YHIEZQHhO+uqpr+TPg7 z#x@qQ^T(!Z>bzCSexI&tMD*X0+cuI!A${xFC-n!tjne8maU$LtXTZR(jWc%r<_*GB zi7}3ukIM`5#PMYvThn%HZk9&3^--(r+BxO)S>kl+mFIe*V4b~Kn&k>{eD1;{Jqh(- zk&>qvv@TEnAJfP7Y}-SMw_q`Wp0M%kRof^1+upQHxZ(lcJ`@YJ2O#9 zK|YeFhir`qO;a;vxp+Uw?krS{pO+9STo!7c06AU)(dHLxlf}U;bWN1%4i&^1q(^8G!QtIz(x2%>402d>f;; zXMSjSf1gz78T%so{~o8h_l7?G;*|I5_@Dyw@Amw|B}yKnR@t-jb5zc$%P-jqdJY=i zIWtEg@vaePD1&$Ru3oyxiDF!L|E-KUn2k(dzas99sSbPazc)4^jwxIZ+sX&xRJK84 E01*7DBLDyZ literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/brandIonos/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..9a0bac3e888595ea7279c8f98354e6714132e4da GIT binary patch literal 1858 zcmV-I2fg@GNk&FG2LJ$9MM6+kP&iC32LJ#sFTe{BHHU(>Z5W3?>>UgdF#*m(6&Y{3 z_5UlDo4=TuVR`n=+$&~=jG050x>2qQm^o#s6xE@*b7x)$|4c1V4dj0TF2EtPC)wDy zg2{Xia{-DJx`3%=j?36rfVT)y2WF^XkHU@3?3bG6On6`9J}`6@oBC|#5_}wSx^f7& zwr$nQ?!O8s05bQ;d4fCl?tQNPXSgR2Y}-v6{dSm{c_z$>BumPiz$RutvvjzUZJV~P z{cPKjjm)-fE3^3lwqI;p*E!>7o0EatMv9UhEgaMAy#Z3SZOfEEGq}6EySux)ySu}h z4D;{VyHmp5<@?^5KM7Shli~{kE8=`r(3K~!E`KyqXbJ+bXGsJSkby%1+R(vm003l@ z|8KButG4ZMY)89}129rlS%*Ud4nKE?+07)O{|wx=Z6rBuZ|{2qx(Wah@JAj{5j8-E zK^$mIQ@}rI?CwfI+aBG*VVv|#PG&cMa1Cvf7t?N8akAzdv$=e*hxY^SUB;EJZTJbwM2-e)ZvF}Cq?5Y*gT5vNNO5vTn#`Jcp) zh;I2$W1>9}AabYfoW@n;bNY`@GR4#2RPHgvz)LWBRS&Pfc%8Q7cKBs)2JnQ?!Wb;~DG$Qvl;1awn_*wiu3XLTl(x{yz$TU{U zL-tT?+vyWA)Mc2XK%cI))SclG45$Aw8B)<|Fp)fO?%eGHNkS?KmwM(is)gMEddqU$ z37YgL0Hc`f*CKmi>|BY_xbzS?P}r4FM+C3}v)KSp9wO6(4*&^Y6vw#NLgj}JdO1V7 zXaZUf{jItQ9Vd4&Wn6}dP!Fhx#pDvIA(5Wmv0!^bJ!BYCN?30S$u4S?8B}UK#L9M+ zAd{vmpBxOoa;%tNK!lkgX!-h#r5h6_RKFn6+(jdCE++QQnRHXOJlqx$#qT^umc7Il zr=skXqYjWg^;X9vd*P>%DF~(o%Xa?L2#Fw8p>}BIBeE0b!;7KZ0fQJ#3%M#J2db-D zNE@UP_Fz1ndv}@Wh=rI964~UUk-8F$12j!XoVENe3?^`|vKlc&8;1VfZ28|>0h;-D zhY1f&uu!%Yw_#UQ8fj7os2Sy0CCeyh>i`A38HPA0;YyD z04RtZ`jW$FG7+10z7f#Bkz~B`Z|Ihb-}cQbLb4)Zfhke6dVNLt#p6 z5sPDCB~n5NXXV_4H4B6_i-a!^9Dn0Te}*H&8OF{dbg=YmaUA!vNg*YX{PN>~*!8(E z<_uQ?A2^$r8XP|Pg5YR6Uj8)*bMI~=D1sQ`Qou5~?)Vy6Al}3%GJzsWI6KxVc%vS) z@dyH@NYVV`K^qYOx@meM;8`Y1!foCQJ1PwW7XTI}f2IOY2x!gg4l7CspNa*Fb2keB zHHmde>dGh#iNp^zuvw5QS$kfQ*Dz#K6m&(cjsxu4M~;@+lTbbC&JeGugtRD(Uxr^5 znA1glO2-BJ6_`PuJ54Vh2v{iq5D@^58FGJ{B|h8zxi5=hF%Cc^YbMh_a?^06w7By0S0XeuO{ z1OP_Ctf;#vsc4J~Sl7l7*;)T!)&s#c8)u^KL)QFTBIYpw^iqL@^bk26u@&=oja5?jooE$;tq) znEJ>#?;5$NaF#EYJ)mXtNEQII;#@C9(uE$DMn$Ds0l+*fpLl}X0kA0Os`*Gg^u0H} ze@Ip5_vIo0mbX`++=|-)FwO7NUQ6MVGF}?rKa?TU!e@RT_f5g&ctUez%GOaOEM zuxa^u8=5zMHx`y^(TFX#G!8P;_`(cwd+N0pjM#Dic)JV$+uEwQJiR}cEC9;2m5(;~ w-*ayMbL!vf^E#mX_wOF9tbHzOG`1?+xW4ZM$kq1Gw4DozZQHhO+qUhLwr!(I=cGz)C)M6( z*6dkx1?k-XV@!*|eRjdk!FUDPQKabIOw7dTkz)h?{g@8Yrf^JO=53>@7hozMzKMZr}OJED$ zT9W@(DnoCj!<7baGT)Lk%te+n$hkmz)NP|ZiEp;i^_8O!z3E(iIm^RRZm}(GvW-S0 z22CPRp|;J8tdJJIC1IHDAQp+onnYhD{;|#cDsbYWHp5J^ONYzDGOVDYCeb&E{(VYZ zA@R5626FPEBmq+b;obQ1)N zo;gs%6&FG2id91F5++Blz}Fz0NMXY=Dp|M8Z$IM}kQrk(gu=6;OrAm*9F8To;45vO z`Wj@)1Q~i3lu@f#W(Fu#Y?u~555_$cM(^V^3zQi9Igd( zfl|V@ZYh#rxDWdtBpodH7PAC~KVz7)sNiLqViB;|MMx}qYTj4gS#~vX227NPlckU- ziv&6T^*tzuN<8P4r&%5riSVH@OXX1U=REq$h#VS(Nh^->#495L7sS^F&48- zo2K!_?K19S%7(H$edu_Zlx#E{n4C4+c;bDO40Czv(Cszs7wYu-^hz~_0}J1T8EE|Q zIZ8|w>3#RtKv`LNLdizMAuGbGJg{M^hk1;N*eE=eI{U8Ak+NXVPk%ZZoBNcs_YoUN z9jBP#Rp{~&yI%P=pvzDgNgQkFQOh}|uMpf~9wWSFy~C`+EYy@$$NB?l1> zUnMA9vl!+D8r|OmW$xQbHXEs!&tZ{c-nTQjuJ>2Q+56WkUOP+WbC~3KoTnJml~u}k z`~16`gdO%eNpiFp*Blti;-~7pjiRDrjo!MM@ZMa17OdKuP^}K<>21M+#oq-&GKT3$ zc5+5=$`|uS>|~g25oYDjFMCSxDgV90+2H`3kqIjh|q}?1R17{kr$Eb z0D>Swqxg_A0Xw=AWly!VF_4!jOfQoFq98!G4wELM1%zn_ql>T&9%l)pfwsS1WyU~K zxE8E92IRqW2dam~kr^ccEx{dQEM1f#N!VBYIq(5MTY8rFaYbRgC@v*&QBY7QM<38y z`1fr$Hybx@Y_Yv9d3-8xeEIzObKdo59kpC<6Ke>T+`~_1O6?^PmN)fPMtC zsn5>P8nCk^Q-LyI7HR}T(4rV(4zxfWFatHTbw&wqh$c`7_J%Q=2JBo|eQti=ENFpB zuy;T`b|$b#Z!b^^_8yluGOEkYqUrJygCJx~qI!qyllM{r}n zvXu19aVX_9soDi)q?V3}L?@LPQJxdpqg%|?!E2vux#^J@%-~;Dyw%xRF%CGoVlf5+ z3qa2-i?Zbe>z32M8t$!|t|6rfD(%-;41f~0&pbJ)nD4Al3@LD8eJ3kRm!ZcX06+y1 zP7DC`6hIs^tpe&SA_2eePeD=mFpZ_pZnVumwEzaO66UN6*tt zZ6udJ?0t6(B4Pr3O(jUL zt}>H1>Q3W**p>fZapdZsEHm#8t+@M~1=9=o{U1xy>ON|1IsTjJ0WhT8$Bszlj5Fpo zv-PhsQ^zthv!@jL0pv9^Gpo$@L@sA-2wfW*Co)62iVWe6n3->EOjWvkWDf5GOmvw! z=E;>^HdD5s_(CQ(k|alxLVu}VfhV7Womt)0J!oe!CL~6Zq)1X8&oVRb4Rcd<)yzF8 z@%p^zvLHz|ZR^_Ga9@)r0T5Y}qqoq=Ksw54Wfgalo&fF|e@3 zz`}`4-EQXYJ{SMtl6)?y=aMci85mGxXs(&Rz$JCxIP@cD&i$7PBC4XBSQu8VMX$-C zpT;fMxg?tx>E{we$;B`u5k18vS--exdc&3{Fd|ygww^ygeiE}h<&qXd?117YxMVz? z&SaQ{h%#eXf+2LHk*iHvzrMtk2K{!1+}(aIDJ3GA$v9lBmX^#qnW#jx;CkC7rl5+5 z1EVwIMJ{nkH&K?0$ILVl>GZ6m(|-OD#GAZF9VC%$iy9JrP)wS;2w z2Sq*4#q4QtavdoaI2v(bmWf=rwkMTqnMZ65zaThbdZ^|CIE|J(GHIE7hFYcW{=Kw7 z;4jTWSIm4~Fn`jz3QlL63gY`h+$$Gu>bfdg^wCSJ&&3^rdAWsiS}V+4N8HfDcKR|l z$AY=ezic3UG{UI3Uqg1tO!5R?KyV{&(_ZW_9bz*^PlhJk89BjZOPvOMSc zEUZ&wnB|e&66oP@d#Ke<0MM1Hj|9Nb!~e8GzLQ0B8Y4$%4>IiH&%@wW%MsHe1#eBE z*8V>l^HHc?%a*4H=SXwpkldOBReS~cv=2dE@P6F3S0PlI21qDPLFe;SqM}t-^}aon zOj2cz7?T31;wQ)#8F$X>S#Z0BY1#z%bPiWj+wFPOHKKNojJUYPw5P7}G^pZVNMo`G znZz3*tp=n`h2VsdYTNqk7i$B&h8;|i+|p}Q?PIcseMHD}-%i{1Y6!gw;R~;7xjK3V z)Xb$V7x$UsKh+dpWK!q9odmXefW07EnEpP8hnoSZ>bbNtN5c+i7BueWP9}HR??Fhr zRi$}@Fntc8d&+>i@oi;ZXRzy;AuqgBslZw0%6hDQJ?)hoE^u7 z7Zoa09|hdeZ{0f^QN2D)%|rAmB)}6#pY3AE*C{LoXB4igw)1OaoEnCh_kgKnXH&GSfdFsHnX*{{uj^2% z0$`-74*@{-00{JOzsL+#CLu1)Br}36fdsSu4?yC1Q49mXi&T5h6lb-f(y$Hq<@a{q zR6Pg4AEpqo0Du!pwU@G4RgySrP=c83NsBW0;O7!`OWl&5Ji37O0KoE1`3y4cc-s|+ zc(~%se{$r-MGHc|JzVnFis89=VZZh2#LS)MH@+5ih=w!4(yLGq!p(N~+BZHI8WQy= z0J&~TH-9dtH1lPVP?uYTB*DmFHn{+_9`&%Ep+6B5fH>Ur4U6}d`vzr-fmW}w%K!|2 zv#wpLCwH%hM*QA4(s@?zb;&Jh+6+eutkK4nSw&Juv3!TLaH!h8SO4Bu?yQ*iIsojB z@4Y1x)mgo`C^;o>(1Ds2Qy#C^@z$`!5*zoi}Xnh z(Z?~fN8MDu2cSF$V*nHk0HV4%n=6T4eB(w^$Q!zgljp<^y!@NPB1xDeI=@3&SoGbW zD}NAGVyZ{Cc?Cv#*g{#BA}s0+UBn47X0V`*EXzP|N?X4_Esi$(WLcF5&ZdE09FtB~ z1aTtC?~R0It05{Ahe@JsatWc^p=OINZhAeUN%ervkG-oNzJ(kqWY>uX4!ZnX!Xjy~ zrT;}op;T=hp9|juOcX@*YXF*otgbUNxP*{mjOZ**2r!04qA*F+a0#KCP`e}#R{XU> zGoAxL-089P)GD=-BZe#ay|Jq}!5AP;j8T~+*h&nVp;YZ1pQ``>V&Kj~G|hlDAAolM zxjK^*&5jp$`yS#1!3GxFB?e6`2-RzR^~<9anseArt>2&4k)}$-zbek>kDa6`h#o2v zgo#1QGMHs~tIvnyPEp+=>$$G@Z&q#3mAVP!2qJDI1-t>6*?`Nx1uWJ}Ec!*NLGROT z)0^E7OMyZwj(7)vQJ|M^a*a}y#ht#JI8nh0Y*<7#>L%1p>8IblGXl{v1CAU3s`mGR zOC(g4u}SRu>m*LXEma?tZ`k}A4P;%@?lJHv02`h{YkFNyGcf?&zT0KK7IvH*VTif0W6x$~?Ip)d}6^mPCR037;H<*(bxNsv>nZqEJ! za31>lqY5z|yRgG;qy%6Hz^-;pU%r^2-(~28!I|s1V~cU@RZHcJ(1T9d3Gz znbBx_qthtEwH>QoALrK|`ckaNB?1#E5EuZk>Uljc_K&(sH^J`4H0dVzM^)YPa#;54 z-VfBPHW&b5+h?E;z@ptJ@UES+_aC^uRJIT0k_%W;H%r<^94ySjD>?{|JpBi3&C@grtoHbwpQrMAy60{~`~4&P~L)gE35WLfxu z7NSzgu|7iIahy_#s3lS@wyN5{FLwA&!z;}PzVuf^n7xG&$!*#`zYLq#ZUtQXoX_iO z=j2^5o7xw36K*G>Qe!zv$0JiSfw zQNx&tDIEO(Fwh19!)5^1t$Pj4{G-VZW>axlC{=5? znU3kExopDSeWUnSH^Hq)$ literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..ba2359b116425952a06b5e2f57d76606e4e8e2ec GIT binary patch literal 2364 zcmV-C3B&eMNk&FA2><|BMM6+kP&iB{2><{ukH8}k6^DYhZJ3ll@3R9SBGk5R!$2NO z${B1S%g}JkzUf17tmds$Ajh_?Wm0a^?(W_HD*wOkj0rIRk=0l0|8RL3PE18)?)sr^ zf5rgZiQL`Y9f}FKOS`^BMCy>$0C4xnaU@AjRQ_9q1DJ54XLne%*6?r`+qUWK=>OQZ z?XP>n-$eRJ>2S4eKi^!jZQHhuL$EVtO6negJGJN1t7GTTQhJ6)==XS!!prW(7m zD%+m=TaawqRueJ*F~r@7;O@>3n*Uycd$+u*RRGDh-A1uOavCwBLj`iCL>~;tk|lk0 z|0jUDQVT$UKN9>=q2 z+cYhNY$?FewylK>O!I0~DyfOCXni!OOd*?U(;{%CYnGa%Fty1&jA^>lM}rDo9(23+ zm>)+3Vi`>={$WeoL8aQ>5Q4KfcDPYS2!Il@Gv%0O;POikfG(B5oZfCIrs-9WR?BGZ z^4n+p+T}3&F9bk2e6JDGKuEAaEeo_edc(YWU<05^{Rd+7&%*LzvQ=Z6Ewv}!XIcHs z_Y-&+2>?ufy|o-wP-Z^}R7t1<;PUE~T}7FFKQtU1urhL+r9(v-LqFV31;DH6ZRc3l zC-$`!?>pXUD)`)TM56J#mfJ^Z4P`WxRZ&4kc?FkVTG&RCrbU@(KoHT{9XQ-fb7Ziq z78|vhF!X`r^1FyWQr{a9nD?DT9cv*cYB7PvKTy&@yq^0`SWusbUTDE|AnB62#~7N; zkwxU5qV83NK=Kb{EW&Rh;__8gJD-k3r;&5M&be7U_hA6B~9ItB+Q1U?Lyo=(>N+OWfnruX#s2DEHngi1N zP5_WIyzkidAp8rt)M|WCgkP30;>>(rWX~c^#TWbDFxPe-K?YyVQ1SpE7xF7`Bo!sF zfQY#Q1g1sfRjh#`t2NpGqz!ZDRB;2$-~&ai@S6yi-an3t7DleeI}yZEx(A|#N0Fv_ zIt_}K=6=n_V^v>pGWcu&5DrBfD^l3jc`x{zIHh_H|NU{aY9JaJEUDu!{-mrq1&W*I z|4gKm4$uA=+H53)*|V;3r-&8mwf9~x$b}tV{jpHj7o5xkF^9&TXVt3BhT^8V-!rNE z!trr+8xg#~`(CU=vpYxtD5S%&Of^b7f7Sxrun~9X}Hs?H)-=zhXo1zU45?Jyv`vzJV|3J9ZB&|yv*ss} z%3@G)%?pzJg=3=nL1VJ!!P;j?qvEC)q?#9=SeJ(p*|T7*Ur8fqShP*5yL>!W-|=KI zQrO;oE%=#8wLK@`uVI%n7Pn^#NSFqXXT+^NpR`>chXDR=gK&YHhiPUp<&K_dm$v^|{3Sagj!SjCrjY>o`< z*&eu{1D;glBgC=*2O#)LtMkE+YCIwm11><^o;|>e|DuglZkMJ)rdraBpYCR zkf&;J;K1p5d<0z=;0I`Nd-f%BBs{=^!?WL5DLRh66MNu3whW}% z0r0VtcwM{b=trm5X7QcgcS+2+FFo}-r1}o8zHa+xfAapTF7Ky*RGo{$Hdd_CfILe& z4`iFA?t@dG!BKbr@qxssWjl`6x?Rc~&Y4elotaLLM3^EFJvT^Htd9&TGY3{@&IL4x z>|RzsGd``)3>32mHfPQbG$7omHJI$OB3Yj@2nB3UIR{ezaHr0|aF+$i=9ED&;G7f5 ztcLlT|3Nt5e9fS6N6wrR=zkCpypRiN87XtN`1t||2~H>g-5|0hb6s{MXAl%X0s45J z#Ncq36FQr725~`Npk)NIAz46VkPo`+jLI1Qy#ivh2X?2N8_2Bhx@Voi1OVrM29m8V^D(yP3?>1*kQ-^^Lgsw5a|ROuEXREN^2^s< z>I^0WKw@&C zFsj`-fc<@^gsiGV>fuO-?Nh38y9c@be6L4F(i21du^a4A2&I^BtF8Va>*k zqn`?J^u~>AoaqF)-Q=i0$VtLKCD6{dJ%FnMn6l>4s!<)k-F^dKh(IfG&F`Xm|C2fP zw;tc=@pdOk% z+++{alb9zRoHC;^T*WeI=WqZWAnFY;M}R#7oYAkFOkFFvl&x7x*=i+=|1otf=jTL# zYd65$`^)p;J4q@7P;Z|DgaCEq?U;dM1z01%uE5`SD?;O*ztt>z|rNsh+= literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/brandIonos/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b5c50f84071a1b5c22fcc6acdc2a8ec729a4a1f9 GIT binary patch literal 5874 zcmV1@$8^f){;@Y3s#;H`W6RgGTCyr3Emv~{G_ZRb-Jy4C+L+mpC0#JIJl9!5-6`W@PPFk#Gl{W)q z1N!WNTlt<7Umz1?i_oRa0O$~*q_Q2TIkZX1G}(fa2~yHQkZn6{ll`xOnVFeca!9t5 zSdR5oZo|x#W?OIDMoup->GhXIO&?cM67Z40hiqc@GfRhB3jl%$t3)16qPs1_V+Il# zrf^w=?k6L-d-A&vL6YRQ&CFc^1aOcegTn{xINSfavGa3Knw%6yA}R9lkUZzmuDlU% z#EbEg?;+38Ifqn#ZZ}~8PAu)xx1L3)(+&(AKs#{a3cP}wmT+2UTit|39Q>=fu^&Ky zI_EgKbTih$N&&}j)%S)E%7VC!tt*}iEB2dYM;TSL>iy6&>O%x+5K zu3-Q=r%G!OTu!ek**oW?`qr#&Xq^Ypz^Yvx(D7#H$lhB}K&>$fV#T@I&*MaCjr`k;YM;KNs4Tx{z5!0o(><6|3+QZR6We_%_P{iowl{aTe8Cp z4LE3wWJ#7}>AilG07;T*TefZ6wr$(CZQHhO+y1uAXWQ=$1W9t^HiNJL0!!r9tq&+S zd238J3mVh?54~zk_b)7CG^&;JetLGwUy;xCMk#z_9A8URE049$^<>|Kd6z9}LescN zt4Wg#Y2!>vBGBr?|GDQaetQCDzuN9{RT`xyJvZ04#~2Mw25Ot(wa);S3}D3oP8q-% z19)Ko9}M7!0sJulD6w$Gi!QtO;n&>t|M$LtbE#Gol&1Fl?44rQpBT<$S=9T{EpzU$0C+e zDnB1Z4dUK>OjG|OL=itMlw4*D{RsVki&FVHHKU_MTzd@Qy(_AhTx#{G>tDByqGPaD z6z6VPb|1F3VW1v~i`d`tI6ob#D3ZcSeE*ekzsa{(DmS`%%PZRG4dSXqEMBM0-_$Cz5XG9U-NN#Bq(G4Owe!;~t zi`^_F!^a#0xRE5SA>wi~?|Uwy$41dOOj!o-C`o!l#N}z8k7G&TVz+n;25>D&nq$5< zxe>NlD;}F$LW=mKBzNCo_)Q8DuXdQk3@l1;-ioo1MVZICrlqXP9f2K7NQ zfsOqj07#-H)Dbbolq&enzywoPCSZ92rsj%`O9aUgtm1OCT7kNgfejJz3x!zITNuDI);*sY zRoNax(6o!`qqN45Q6J=+!4OhdDRPK>m5jxiLL8_^g!lu@G@Do}glGuTm`P2!>^4z? zLC1sC2F^MgQiV7av6z1Z05X5g=c5=V1Dw=2Lrpw4r8NZ}Y)VTAl6JNT-~}NOENZf} zZr&XjOM5?rS(hrTAUFW7CW=?;)lqYNmBfgy2#*Y> zXn7Q8Sdjtg;R0E;Kh$}{szrI!?-g;(5ZYNjDvrl;Hq!PBgSd6AYAug7Rw1zj|24*$ z^4KtGOoRp>*Asdnl~}dEH)g58eMwX$iTefcj`~e1Gx3OiXI=rM#%y+6iE{Qfi;AdO zBE_l+V`jdP6{lPhp{1IMZ)0Ewv&ySF4i3Rs6juDR?*Y~Z1X(VjXpl_Cw7E8WB4grfWpNnRKk5^q#--^SY{{#mzo*G!LGA(=m*R>4q(D29t^d1Q!=cTI3ax=!i=wy)8UZEp_W?T z5@cL-u|!ay8xEQoF$%uxln*@<9D zt;Q6Mtx_BhBoxuVt}2o}hkI(c*xnG|O?!NRlKI_}C0BETJgzn<9?9TCXG5;#7SkJ( z`W1~DWtl5@R6_S9SgG+^HQ%K-e&Mmq<5UKM0vBH{oW?_q6b_0ZnI4c;?X9leSy&w-I1X~engla=%-JQ^a{`R3Clw23JWL}%p((Y~0AR7SN(V@aPmv1r6cgV? znECk9n;QbyX+=W?4BO)haBG1j{RRNswC0SO2CVQXik~@Qg24fpS->+|e6b)9*s*Y6 z0K!Al0Jg$S{Qyiz(<M26W{pjn=!qb zLA-D(m`J8QF8nzYG4ncwfk5bDi-p3$0f5kP0)R(*FmcOx;!Hxtt>mBrpK{sm?Jxkb zPn?1UqEq*<($Iv-))c!RY8B0mH`5dNwMdGT(CQ8@XmKjZ*B=l zcD_^ruLH2Eyy07L}G#~cWfA-dVgs$2o?yCSBnUIRcn$k=acfExw?gf)kYw6N)f_NAhu5h?KTMpAz`q(WCw(+=S;$uGtFaKjG62+sghg&= ze!u!IxgY2vku$h$8&eu^Kq%t-U}=@l0ie4wtz?=4*x&*{ft$|Yc;&!2gD#^f;<58T z7lr*ssJG0QxREE0ekFZaaZULaJ{th(Da+wWr_8IEH?$t(n}oNUSy8;ZpwVN zP6D<#bcJOqrvPBEX?V-|(u&5q1pe~w+Z5J-!w3*Nwv#_e{dVbv8$8vGH5m*!j-Vx( zz?kJ109aG9#!7npG$$OwP3Q5sGIx%HLF2yFxNjN%ITt5l3Py{<(M;BAy;9EVOv(U@lpja_EM;$7vdT%~68y$~Rz-{gO1$s`l8#I3qGL7p35)Z69B6G;h;fqxEA1%Ba@)wkqy#8gvwoaTxa17<`3&=icB0m0!`h-b1k ziz99{MjetD@<=l_(vQs1+)dmL)}4SUaa;qS1OOvS09edX_0bN&MC7mD@V;BM;Mg$S zq_5?~+VufNQz}*s9l7!Qc>>2ui$!%Yeq9@Hy-$c%LmF@1 zF``~;tgP}HJ!U8w$FPC&>luZI81YxtdWMg-Q)I^DU znoVE{+&U;6?x|m9deC$V09pVzu&9=T!aNmfhKYpaFGW!e_q-?tTmNbGj=MZY6(m2q4f+5BMgX`>NzM=)U_h8};+w?c zUky-4hAKkOn{vdlWVp`Y0=J@s$^`<_BP{YplG-kPdkAp zCOt+rR0Bv^0N^o}s_X3o0%1;@OuN!EE!f&k3R}#AzCDHj;8|J2$Po^;&AS)?yvE$j zH-#l(Y%}NTU?6MRp>M?s0Q?slYIDkwHmaNg4Cs3R5HJ?`GO|!|Yg=d&4+gK^0U+G- z$&{>WN`z2iKEQzK!FkV6(v;;lf_qr#)}yJkHb3*8fy{%q^0A1B*94L$SU%>RD4MeT z6q=|=W4~!9X!XW5Q7BX6 z0N~WTZ(80nkS@)ymv0aI-I3mKhrU&BbKjxtwD+C$+eq;ah|vZBdCrxv0l=LF2pMWK zXE6tUium6BsBX4c2fD6Y$^t~*cKwmJM#|j*WAjj37a(PumIVmtyzIuU1Cj~cP8CMx zXh7FJ`?CPSX}{=d8!SoW7+JYI3*evHKXVWHLDY4ilR4Wtut3;@D;zW^k>2Tj-)`i5Gq;AS8gX1${dM~*Uh@o2(6H#%t3FXpX@ zrTJHh?o+QkvhKX|cPQ#F@un>PS4LdB4fKamrLl(u05O0QJ%bAa^2ha)eF(v%~6~b_m9@x_f!6$O@M@>)6+LtezLPgEY(FP zTPEyNgOXMZVOd?hU2;vEL>SdQN7v;e6ZVOrBiogVF?+B&D7Pg$0bt9%zxXmcOx!kB zI?u1tUuJ?R=;q+p&r@L<9a81p7K4o^uY#Q$GTH|Hu zNyT7V?(^-oZb5sdB?}N9cWx}F_V>!tTEE4jzN{oT07V3JDZy3RTb-b5=m#BU)0ba4 z>9})abzn=TeZjije0_4-DeGoISO2W+`->Ow#edKaH=n$%ZHtCmsllsNQ)##N?jSfo z1kutk-Hyc)17U}jB7z^h9k&jQrQJ4owQ|!5=gvM05Z&P*-%n@tcWn_3G9ot%2K(03 zbXEZpE?U0B=F#t1Eq^}vO&y+9x>n5_i}+zq?QutC9l-&D5sC;9(;`8F12kFH0Uj-w zlR7S~;>RgpkUt;%N55nB19zbH8>%{M*03lj?1TbWPVTn#mp8O;v79bt0TN@cg+czj z_bnREnI+w`QRl^|-s{JuMSL_C@!Oc&H7uzeEbDzN>q9J?LoA#9mm0HLGZy*lo_cQ{ z>%1I2vS`dQw*Tlc0m;ofv>fDX-|*$FcS}|TLX3#cjuId2t4Qt!0Bu7LAmM2TBzzf% zbn}6y|4r`usW|!_X^gu@+GFpL?xWVf=Ul(Z0>o~<*`dw6w0(9@LnEE(8;mQ?5w#tM zoKAQ~=55^o0PFDV_N*rn_@(cFgAO{Vg*Lj=0lr^8y?yrZ#TvGrH{zi)4!v{a{)s>v z00aOKwRR0P_BT|0`C>i8!y7gX5BFT`%T*2ijYD0nzEoR)JyRYMN;)pbynR(i znvpKrMrvA<9$)IpecqTHPb+Mh@>8Ov>3B#UjKu;@7K;tap?I3sve(L)ls4623T>*x IG|kmo5mz`V?*IS* literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/brandIonos/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..0a5b45922d7ec476dda16fccb216761f3ec4d142 GIT binary patch literal 3000 zcmV;p3rF-)Nk&Gn3jhFDMM6+kP&iDa3jhEwzrZgLHHYH1Z5)UDkA1!TBVqz_VI7=h zRgj;;63qn`Avc;q60*<(>*yp|18a6l(YAfPlm)rFyDRSQ?(XiMtW#|RbX@?u89Cqi ze!qs3Sb#&EMEfNw?ZY+B#TJ}J5M7c>hUT2e2Hb56B!@_zE#Uv_qB@aym`e2x*fg3$ zok%-tz@d_+5?O;obQ}Nxq3plI*|u%_vTKT-P%BCjq>4)592y0VhwqxI`qlplpdOk@2hKO{6r^wzt0h11}jHcM7@IT9_e3_N`3W}B~WQJ_39^S!?Y};*e_rEGOKuMSM zzP|kBZ*m4TjvGl*r2OpeF*7qSLw6rLasHg?aBkZ+^?d%m*tTukc2cJ^E8F=4>g_7& zY}=~r`uV**0aCN=b}gl#0jipur<|N~s(_q5C}hq#Cw0?W`?=4;vIW! z1nyhk^FHqG4jV_ZZ6~U%#wCu!EEzHC_v3=}e*#*y+$Mk+_(?~vzOqs4RBrg?G}Rd@ zr%@2oWvUBQMnp`M+?3hFBVN_Z@GriMh*&1>fWM@On2^P@VgtHtm&sld#Rw*2n2a{~ zlT1d=UV8I;#A1=NG@@Sb(YoE&nG6DX`hUbUS7}66v{Oo%D9R;@a3ZLi@w<2LmQu1J zU5IIpiikI*e1k~^CWSC&nUt2vSd}5dA~#f|$0d^q%}Jr0w?{WY)YE<{5-(+nyT(e{ zoD|G?GFEeh!cU_zCxieJ-E2d?krd7c;#ClC@jif@h>tSqCP`9e#Um@Dp2C-Y9^K#r z_{spJx^!4JCk3?mXdwJC%jSaAWnFq8cMFMbf?xy}0P0HdU|WGW+c4>ynC#X~5t_o! z;pe5h1*CEw5}S+SasQmsC;<~7nX=e?1=L!^#`}l)wlpq^DdW2aT$JDrkA+bcf_+?qQqtJW0P@Y z7|qK`g}JIy|+{@ru^#5`|R}%!<&y{NNLv z$1NhT(#$nHVsKMV34)wUoaS5nVR*fQECf53i1t&wq4iR30fL@gM*Df3k=8;2CnfRw zjweRMNh=Zj{oYM@qTLaa$1)f;+vA7uygVeI*%U&!A$k##Q$>`@ys)x3iM&t=VkT$l zKBW7&katSO-uoe;F1h4dHk`l~ePh zST#8xKO~5bq%*sOP1h7?5S7M=UNoFVDV-9gis5_Nv!)*^MEN-uGRNxhu7K<6mQGQ4 zs3d{gVAh-?=mR!vy&TtVrRSlt=p1J=CVpIKeL8Nt9FCPH-LMz!?Z!h>d$A>}0SEZegy(WC$-mW~- zKu+mVdB=UPNv_|^&Q4*WViZOfF(ahIWjVmCEl+t%KT`s}z z7WuxCZ#wXn#-Q6Aj@69e1b^bTGHyJGdt^uK7bEp-nhctJkKpTZnQ>!zq}+_v^U%#MHsnE8Qt7O(aTY zCp`jpee;p2ldISIJIO1Qwh{DRonES=H``l{A`Bz7kBZPhJ3FS%u3f9U1SJSCvRhwy zWWXl2o{#GAx}9pSjns-#!^pc0!M?Noc2K9gR>vKvyBKBC3d?^}$4}NGE-)#aVpNz$ zcJ<|NzlnoBBpwQW1zu}bdd*rV`8?=)G(3C1KHz^y6QY;a8 zUCPJ9f-7GIV#0Osc>KyC*sT_*DdVJ(u)FeD;3j^gHH%!>SAm_-cq^?5YVG$5{KT5M zRRGX>^JI-+fl%Iy#Hl>86mDND1sjCWzg-ePrEu^mN$k4$QEDFvnIj^Ure__^%TI;D z!T)N=`FM}ADf4N(L_ov@z^qaK^tLYkZ~XaF-x{rro@^caKj>DCZhM~FWVgQeD}OA2 zP5+L*#Wb=GMWRQ$S z0oOuuRXiI+1Djls9M7d_# zQ$hLEpVvD83ll)80078Tjmj`M!0Sp0b!!0?{O>j~?PR42mZ=P=f94fU|0$`PTKdZ(7RwK{rqPx#7 z6%FTXpx-d$zNpFddR0wI78gGN=mBLb04HxhjYL|450{FClMRIr*J(Su^F-kX9#GW- zXaf~<0DAyW?|fPew+Kg1X2l_U^6U=RX}6SL0F*5<^@<2W+v83O$Z*Ksuu;Pdv>ig{U~gJk^xpaWnEc+LRs8u$!-OlrM(x(|hq zuDInmo0P_XHf{wXEk&^V@aFzu=yg*6+=MT0%=^J29#CC20So{v0oN69y`Iw`XvkyH z3kJT&MFZZ$QllU69+VAy4~yRUy>spLUuWg)TT>SC zfdnA?A!FwN905GE;rePgej50$?G(VZ`KmS?zvo(VcIJmxLS9ts519&p`E5G^TmifQ z$5$JUzc!ozZ8(7j|Keo8@mYIj=WVMhnIBpeH%O{ARVx4({|8_LVAry9mf`PgIsEN^ z+Y7Z+t0Lr=lvQ84wNTj(V7BT%!(Vl5d!=w|soqkAT$5_8|G}(f<^kwds|5qER;%+e uHS>r1Rz=7=k+NDZ*QZiH7PXMM6+kP&iB|9{>O^zrZgLRR@E%jU*-fVRyc}B@hu4!0Vnp zYMvn1_A8{|pyY~J!QjDQ_8X$G09Rng39G7ZTVVnV7-PJCf zxTuz4W`;?`p1@IIW@g9e@l2rK5S|SxM)|5v$kEQR?E@_{>_&&dPjVHmj-L{RK-h1!Wnpkpo zMEnncz_cyQerD-%1OXrb#I~($+itRLPrblRoNU{+ZQHiZlPKG6+&1?CAq{|#-Hr58 ze}Ell`zJSYeqPx7qJ>gR+*ws5OS=CBr&uf&i1L=q>XR z7&+>YM=+E%z%v*Gy^*ma1N0V8){&x4y%9cx!k#7TloR?01b7HuqNAbLSPfL#sWJof zdV+!?(OYhSof{n)j+i;3S50R43?n{ps0WfTB`dnEy~ZPSiy+crOA+qP}nwr$(V*tTuk z_Cy=ax78Dc2M33zd5zi? zNs-I!{e++S!)9*%tqhmH=eB=j=@fPXaZ4oZSekOCpAmTyO6N&vk61?|aHUGTjDS}W zNubCVZ=7{=3b9>Y#8SVvG&$iwoUuOOUw2*v?Wqb(+YAEVOHKaYX_e%!P73p%ff99! zAmG2$sgKR50@JdcH=w`E{Nwo77cf~xsf@ZA5U>w{Z=_#|2>g>6pCSbUU@Jw6cd|TLs1;@Du`nN=+)^Z&wKXmqJicig<$9_YWKdwONd`ELC6{0)I)_l@#o$D=GVmEWxtEtVb2rUI=_3 zrCA@A+DLiZwJ{xZEK3oZkD#lh>>h#t1ha}yl=8DZSeCQ@C3VxI*rSHg1(U(<<6=ujUGH1hotvRXc zULwf3m~&r}74yQDkEJRC?_>4g;IazjLA3m?!NwNqITiw&s)X0_GE(rT%SeUs+9&&# zEL_(MSgMfL2>hyZE=_@a^XXx!UKrFBpb7$C>YQg2F|xeM{j{nDXkqIjh=Elc?xu$L6+~N!Xk#3o1#pEU@HJ| zLCZtH1Q|&ZaR6}<4+{)BwhIRZdvrjMeo{sR5cwET%G6ZQGnF^1`chSs;n1o(^v53h zZwdXgrM4WY6;Eh17V1lds%x%-o-1=?3fO^40DA@t;u3(VO%QIjgGV2=x=Q#cy9arc z`5vI~C6(VZjYg|x1EFn48jv}esDIYfygxJ+P1V#~5lhN%50Ix|Hy)<%h#4cshhyyr z0q0!H6$Az~+It}$VswTmDsT%mm8vW@vG!0) zu_|n(Q~*?sDa=5#2@}pUdTTZJljI=avFwU^pYr;q4QH^asBL?wzEbT7DrAiQwWeq} z0r=0r;9gOtK@b9#Qjdj-o>SwI)PHNTDJj@=D3mraP$4Y2#lS&+M(B&bN0V?2Se|(Bmz~gksyad#iuWT?i91r&c7#gU$OY-*M6avIM z{sj<~{*=cK251g&4A7ivDO4f)LvR%_mCC7$HjH@e_d^$hfb(>ZyCu)uQEWhUmyW(& z*TB@?+P~(!!jo-{F;e=|9p~FzAlRVluPt>6G3Z>Tap!G}In!%|0-T_l-%y>|$_CcD z^E(hg8dY=U6Sjp*(e6^p)cpyX4V{C#pV`|A8(tU_Z<0&k;*UDBYalpt(8v$IF7)I5hquIO~8TV!^CuJ|Sn0OR1a?qyV6r4YnG}RIw);vaiM@@x-&8q&?!JA2KRp+W!q9^7!AY~np z^=J?ezYPXa=$Zy#O*Xr_Q9&CZV?cfz4aA0Xz@3ZZ=*^AMX)`-7YvgP&2H`Li7?3&4 zr+7^fZat4wc4|RydS1dq$Oz8F8ASc50L6gKVFbB}3GVI;&IdLHwcraGi5AbSYF$)F z=V{H^!D-pS=t1Yii-lsu#m-FMH_Xa3FopAp!Fs@^pgxPyiD>J@B>=JEFwb!~w>3q} zeU4GwVq-vztU5CN$8D}VOVgT3^M%3tg-t*rp=8P*h+I%e>n&RDHTswx$Tii2Z~3wDh(DZW5sdhA6JBv3VPddHU_SuHG; zF!Q!zFz(nGX|*-O=Pj-w9Zv3iX0TqMaD=I-;|R`d=6fTf9 zYuOvx!Cq)c9vbgreJ2EJG>u_Ox7Fv{NUXBqG~DdW3}3dmBw~uj}Xl11>cND;G%XBHw;bc$eNH)-geh8N-y{VhPQHWil zgin5EVhFfS!WQ7J`o0n7$Ij={(Zq~vgY^MEN2#FeuwT4EYeE0 z!gxf$iR;Q#aP#-{>eAAr_|l-<3jgR*Wy9g(Ms;}IvwVrZ7YS5ostVIYkXHm0tD9sn zMpA0E?_z>=gqgIwfPVsb3^te!pL*x7e)BLDusP=N}hn4nSnwse0 z8g{c9$^m=h$JBoBTT(OL3Un@X5#cL7#mvnfpcp}ZqF0x$zB(wU!hQCN4HoU618ZeP zjl%%gw1pZV@CQEWpnSxo&UF9naw*dx?jf?qQiTnc+`s-gwFAI*f(j>+>MD#8_#2-l zA_<7$q-PH3($_?dYlCzdEVhG#Wn{E{jfRge|~wN?YOS4J8}Vqsdp_RRQolc8q(wrN)X2Z)%(*P2flYrLxm z1G78xbIZT+OM2JL#xqL|b#ADAU{gccQJM|h;YLzyjcNE{%!7AE8q4_I*k`YdH7@Ph z%)izVF_rs~RC446(|iJI5xk&-&q>*$X?@yx5fPVnBX+Ubb%Ka>b`&37&Ue5krC`uAEE}{#3{jgG9gbQLGs3%w(yGpq+(6( zAQf2P#tlH&7^flncz9a2A&8|CBur|&U%GY4XD|i;vG=3>K@U+51U@)G}ni=+}&v(~fZp_TLg%`@iuXy+%< z!exDI!=0ygTJ|RHXwwmnL;BGvt?QS2ue(9?UnT_jYReSDzTibS?Xcti*aK(mVaFjM zh%%-SpegJ$0O{&c6O7?}mjH-j$9fwZ_0i{|s%%tkIurAB*Fdr=K~+YZJ;|XFOAdWY zUFY9(90i*RW^gQUnx%IGbUlh>UIB`RheLNC&; zNY-EEUF1)N(X_lyG0mvBRcz}Ud7Ybj|(wYbd#^`X~v7h4Iwu{dRN}4u>8QMb6 z0Fckt%@wl*4;%$kdQ%1Ncw7+cbA_y7HMJB%BeAgVb+;5TL+}?v2na(BEw(a--X|jF zIn7@Oru}D+p{D(N&)A+$jQ-`6kw*7?YEbh=V~I+843FK;wo(LUe;LXe2kZ6xc@o!g(2A2{** z$Q6^FCl&!9Ye;^FTNKn(5Q)Jn4IuiXxAFoAaEx(+y3wPxh>LvGF>Sj0a}f4wyid+E z`RZd~q8OsmxFQ5?>aEAae<4&5Rg2!GDE99D>_sfPnl`=U-}PQera0YW^wrHGL@^X) z2!MfTU-Ba%;742nbS|Fmy!N{Ocayqe+yub6>)eYa^jUZ28GUy%e-cAb8q}vB&zpo` z08Bmldpnu<*t~g;b@St?o-MP(L-@vM5QU3^cCszKRj?>n@H}GbN7wQ6N&yF9i#f*P zsvm#?s&+Mh)XnJVkSYV#zPyo%FJUpp5$f6>EcmMwtIfAg;2=`$Z%wc%gbgAvTKo;g zX&dv_Tm%cFTkmGGd&?4vz+RMe(8RmRM*&CxP{Pa|%U^UeYAZewwf&~QdaEE&2o{)L z)l_-`fXbqimjs$1#5=FOH~=|itK)D9U^s{uW~Iacw2{>aHb&G2^sZ_4*v@MwF;70{ z!Yr<&a+ta^Ob7A!ms3f$p|=VoAs87oja~(ymdaYFF^gS0ubpH~(dz_Ln74v)H?wy0 z4k={&ueS;y!N@ldxlz+wJ!_g<+j(uJTy)gN@BerKsG@3)Ofelq!BliOmdQKf!%99R z1S8{uy7gxPsClyu+*Ju4-zJB8h zz)rH-v=(umedP2yEa4I$GX5nMDGIXd3`ie&uV4j3@=&#IU#^%XI@-&Nh`@z)rjd5W;5~M}-hT0$OrIp z|4RgUkzmR5NY>~ZWykCj7AZJdq4`KL2S8I@>9iqvT!;$*6(M?qRT^U*ziDIMS-fDV zLmV{&#i&<8xkTj7hA}D)o38T7iO*|Y93CBiKZrHFoLG%usXLY|)U`bYfM`Bq{z&Uc z6XNJO=LceC0BSpbJ3XDt%c~#_gi1oN)B#fub?t8Me78o!@AuCo@nWP0VCUktCsqNV zzW=^h>9c$QV@&y|rqU<-?z8>!wkJ|ubW)w}Qmx5)w>^>CZ1?_f%jGh8ve&M-{?6b9 zO=WMoj*Hjq54>3_5#5?tWMkVCX@vUmeA_9P%jEf9yW-ikg}&>&lEBcmhiGm@Voiy& z=)+08>(=5`o9Nf$Ww%@|-E)4E(+k={-*T98pJrN7OLJ|-*+GEHf3)_7$Y{+bq6!n6ZMb% za&zaq(N{HgWNT`OwV2MMHbB|ilRMvy;pThB{LxkF9L@sfK`f8~vZ{15s2boBaB)aE zE&)hZP_}ZZ9r|U$>Gz`bJiTNS+%^e_Inv1?7J_vno3U+ zsyGkP29gJ;T6<>SeRf!py%90DQLZLSW@o}&w*rtl{+>-4ASqkfqtEL1=s5F1Q8dO8 z+9H4V&&5D{^F7DCx4Ou?vSGX{n<@a2=4QC-{6uI9zjYaT)S=1v(Z=i5wZGGKCeh)| zKNnJ27f*6VOlrg;zc@QcOqdK9W1o zAa<+*5Ka}2Tt>uA7UZw<|hVzYNJ?N)#PUkvAj{or_<}x!&G!Ufsegzh6tVDly*5mJGG1&y~G} zW!XzenReItiBz}#=u@_)97!Nf=SL9}YpT{9?>dv1cIMb}!CRgKz(L)Eu_^#;E`l5Z zA}xRrGi~{vrl}h}hnQM7CytZdsc!9g`TovmXPoCo2-cOfkU3;hBFI})wTWmZECZC2 zs@|W_oBVqgmw>qaTZ`gM14|BHR`&kno4ycUiB8l~w-Rj0GILZi27o`kQ5gQnO1S-9 z<=pJJKdJ_kc~Okp^V&jmpF-6_SNLbeXm)m;Npw5Ba%{M;k#@!`Sawa|Bp(3wYPMB8 zzHCl9&ZB$(T=r|7-qZWAX-E#Z1O%t;L&U}DV8f6+kUVT?y}#c-7lYp?vX3kuFSfE( zkB{i7cb0?ZMUhtG_;OSRA%)xbh8XrPpBznHc_c6dFyd_2*Lm{(b_tWPACpP##Rv3PtUdMFca?|sfr2S!$ zU_e#`MTWMCi_>DzjZ47jv$aLMQMKmqfsxkieNHazaOO~S;&^c}GB?k&5-H=rERslT zqp)^P_7W12v_H_hq-Dz9?A8?aP@na0R4o?Vq>?yJd+Gb*Bf4B;^{hS*zc2q<@A9qK zd!JmtKxD^&NNcUI(X!0U&ebHF_KuAVM*%`I6FYuevxoMLqD$4SEpNO``RAR_PvH_^3IThK zKCa|`TFC7@m)m*r{hT@7zHYVu#w$9$&+s>#CTi=4qQh&2;$qq9s#9e{Eh#ktu*C>@ ziXbM=9g{)GnQ*w>KE2~dj6cwQB#Aw1UfDPD9#yZG-llHsyw>Thhv}R@*SUP9&+;9T z*TrrI(qMUZod1kv+u)y^sX;H=eMW7&?YP#4zE|7=Vo)qYsJNMBudgJ#GY{(;0U?T zOk~R~FHu^XSPnUzvX@YIy@a}cW7w+Qxc=tXIh~>9i8Z3;ZI!cT#z*KcU>M=G+)Q|4 zRRH|ivOl$vs1-)maudU|A%GAFs}L3?EHpbjky{%n)Djyhe>v-{#ykW=rH=~&Y%}SJ zxd8A15Qvo1t(148m>8=S3Wei@5uIvdiQEmm8dJ`Nj6z(CT<9jNEZlJ}8n6inO^AeBhcCeJ55_4<4)<|0`vHeauM yl04reY0Wh%CJrhkEtN{863enalCdmXDwQPjT5H<-R3(yeWD5|+5k~IkkTnZeuy5!9 literal 0 HcmV?d00001 diff --git a/app/src/brandIonos/res/values-de/ionos-setup.xml b/app/src/brandIonos/res/values-de/ionos-setup.xml new file mode 100644 index 000000000000..6b936e23c0e7 --- /dev/null +++ b/app/src/brandIonos/res/values-de/ionos-setup.xml @@ -0,0 +1,14 @@ + + + + + + https://wl.hidrive.com/easy/0153 + https://wl.hidrive.com/easy/0092 + https://wl.hidrive.com/easy/00311 + diff --git a/app/src/brandIonos/res/values-en-rCA/ionos-setup.xml b/app/src/brandIonos/res/values-en-rCA/ionos-setup.xml new file mode 100644 index 000000000000..94b613df7f78 --- /dev/null +++ b/app/src/brandIonos/res/values-en-rCA/ionos-setup.xml @@ -0,0 +1,15 @@ + + + + + + https://wl.hidrive.com/easy/helpca + https://wl.hidrive.com/easy/01112 + https://wl.hidrive.com/easy/0098 + https://wl.hidrive.com/easy/dateca + diff --git a/app/src/brandIonos/res/values-en-rUS/ionos-setup.xml b/app/src/brandIonos/res/values-en-rUS/ionos-setup.xml new file mode 100644 index 000000000000..14cd551cf9df --- /dev/null +++ b/app/src/brandIonos/res/values-en-rUS/ionos-setup.xml @@ -0,0 +1,15 @@ + + + + + + https://wl.hidrive.com/easy/helpus + https://wl.hidrive.com/easy/01113 + https://wl.hidrive.com/easy/0099 + https://wl.hidrive.com/easy/dateus + diff --git a/app/src/brandIonos/res/values-es-rMX/ionos-setup.xml b/app/src/brandIonos/res/values-es-rMX/ionos-setup.xml new file mode 100644 index 000000000000..2983cdef2f8a --- /dev/null +++ b/app/src/brandIonos/res/values-es-rMX/ionos-setup.xml @@ -0,0 +1,15 @@ + + + + + + https://wl.hidrive.com/easy/helpmx + https://wl.hidrive.com/easy/01114 + https://wl.hidrive.com/easy/0100 + https://wl.hidrive.com/easy/datemx + diff --git a/app/src/brandIonos/res/values-es-rUS/ionos-setup.xml b/app/src/brandIonos/res/values-es-rUS/ionos-setup.xml new file mode 100644 index 000000000000..14cd551cf9df --- /dev/null +++ b/app/src/brandIonos/res/values-es-rUS/ionos-setup.xml @@ -0,0 +1,15 @@ + + + + + + https://wl.hidrive.com/easy/helpus + https://wl.hidrive.com/easy/01113 + https://wl.hidrive.com/easy/0099 + https://wl.hidrive.com/easy/dateus + diff --git a/app/src/brandIonos/res/values-es/ionos-setup.xml b/app/src/brandIonos/res/values-es/ionos-setup.xml new file mode 100644 index 000000000000..b418406e0c19 --- /dev/null +++ b/app/src/brandIonos/res/values-es/ionos-setup.xml @@ -0,0 +1,14 @@ + + + + + + https://wl.hidrive.com/easy/0131 + https://wl.hidrive.com/easy/0112 + https://wl.hidrive.com/easy/0011 + diff --git a/app/src/brandIonos/res/values-fr-rCA/ionos-setup.xml b/app/src/brandIonos/res/values-fr-rCA/ionos-setup.xml new file mode 100644 index 000000000000..94b613df7f78 --- /dev/null +++ b/app/src/brandIonos/res/values-fr-rCA/ionos-setup.xml @@ -0,0 +1,15 @@ + + + + + + https://wl.hidrive.com/easy/helpca + https://wl.hidrive.com/easy/01112 + https://wl.hidrive.com/easy/0098 + https://wl.hidrive.com/easy/dateca + diff --git a/app/src/brandIonos/res/values-fr/ionos-setup.xml b/app/src/brandIonos/res/values-fr/ionos-setup.xml new file mode 100644 index 000000000000..15b36af8bb45 --- /dev/null +++ b/app/src/brandIonos/res/values-fr/ionos-setup.xml @@ -0,0 +1,14 @@ + + + + + + https://wl.hidrive.com/easy/0141 + https://wl.hidrive.com/easy/0082 + https://wl.hidrive.com/easy/0021 + diff --git a/app/src/brandIonos/res/values-it/ionos-setup.xml b/app/src/brandIonos/res/values-it/ionos-setup.xml new file mode 100644 index 000000000000..a975a60cab91 --- /dev/null +++ b/app/src/brandIonos/res/values-it/ionos-setup.xml @@ -0,0 +1,14 @@ + + + + + + https://wl.hidrive.com/easy/0151 + https://wl.hidrive.com/easy/0112 + https://wl.hidrive.com/easy/0031 + diff --git a/app/src/brandIonos/res/values-night/ionos-colors.xml b/app/src/brandIonos/res/values-night/ionos-colors.xml new file mode 100644 index 000000000000..5783abe837c8 --- /dev/null +++ b/app/src/brandIonos/res/values-night/ionos-colors.xml @@ -0,0 +1,117 @@ + + + + + @color/ionos_default_text_color + @color/metallic_silver + @color/midnight_blue2 + @color/ionos_toolbar_color + @color/navy + @color/navy + @color/ionos_default_icon_color + + @color/navy + + @color/ionos_default_text_color + @color/dark_slate_grey + @color/dark_slate_grey + @color/dark_slate_grey + @color/deep_koamaru + @color/jungle_mist + + @color/midnight_blue2 + @color/ghost_white + @color/ghost_white + @color/geyser + @color/curious_blue + @color/orange + + @color/transparent_ghost_white + + @color/slate_grey + + @color/curious_blue + @color/curious_blue + @color/curious_blue + + @color/dark_slate_grey + @color/white_smoke + @color/white_smoke + + @color/ghost_white + @color/navy + @color/royal_blue + + @color/deep_koamaru + @color/metallic_silver + + @color/midnight_blue2 + @color/dusk + + @color/midnight_blue2 + @color/curious_blue + @color/dodger_blue + @color/gull_grayapprox + + @color/curious_blue + @color/curious_blue + @color/ghost_white + @color/curious_blue + @color/geyser + @color/metallic_silver + @color/dusk + + @color/curious_blue + @color/royal_blue + @color/white + @color/curious_blue + @color/geyser + @color/dusk + @color/curious_blue + + @color/ionos_default_text_color + + @color/bg_default + @color/bg_default + @color/transparent + @color/appbar + + @color/ghost_white + @color/curious_blue + @color/slate_grey + @color/slate_grey + @color/ghost_white + @color/dodger_blue + + @color/navy + @color/curious_blue + + + @color/white_smoke + @color/dusk + @color/dusk + + @color/curious_blue + @color/white + + @color/dodger_blue + + @color/curious_blue + @color/white + + @color/ghost_white + @color/ghost_white + @color/gull_grayapprox + @color/ghost_white + + @color/translucent_midnight_blue2 + + @color/cornflower + @color/curious_blue + @color/white + diff --git a/app/src/brandIonos/res/values/ionos-colors.xml b/app/src/brandIonos/res/values/ionos-colors.xml new file mode 100644 index 000000000000..8f7b1d38fd2b --- /dev/null +++ b/app/src/brandIonos/res/values/ionos-colors.xml @@ -0,0 +1,132 @@ + + + + + @color/madison + + @color/ionos_default_text_color + @color/dusk + @color/white + @color/ionos_toolbar_color + @color/ghost_white + @color/white_smoke + @color/ionos_default_icon_color + + @color/bg_default + @color/bg_default + + @color/ghost_white + + @color/ionos_default_text_color + @color/ionos_navigation_bar_background + @color/navigation_bar_background_color + @color/ghost_white + @color/lavender_mist + @color/white + + @color/ionos_navigation_bar_background + @color/ghost_white + @color/navy + @color/navy + @color/dusk + @color/dodger_blue + @color/chocolate + + @color/lavender_mist + + @color/slate_grey + + @color/geyser + @color/geyser + @color/metallic_silver + @color/white + @color/dodger_blue + @color/dodger_blue + + @color/bg_default + + @color/bg_default + @color/text_color + @color/text_color + @color/geyser + @color/dodger_blue + + @color/ghost_white + @color/dusk + @color/dusk + + @color/midnight_blue2 + @color/ghost_white + @color/cornflower + + @color/geyser + @color/navy + + @color/white + @color/dusk + + @color/white + @color/dodger_blue + @color/royal_blue + @color/gull_grayapprox + + @color/dodger_blue + @color/dodger_blue + @color/ghost_white + @color/dodger_blue + @color/navy + @color/dusk + @color/lavender_mist + + @color/white + @color/lavender_mist + @color/dodger_blue + @color/dodger_blue + @color/geyser + @color/geyser + @color/dodger_blue + + @color/ionos_default_text_color + + @color/navy + @color/dodger_blue + @color/slate_grey + @color/slate_grey + @color/navy + @color/lavender_mist + + @color/lavender_mist + @color/dodger_blue + + + @color/ghost_white + @color/dusk + @color/dusk + + + @color/ghost_white + + @color/dodger_blue + @color/white + + @color/dodger_blue + @color/white + + @color/semitransparent_black + + @color/deep_koamaru + @color/dusk + @color/marble_blue + @color/dusk + + @color/translucent_white + + @color/dodger_blue + @color/cornflower + @color/navy + diff --git a/app/src/brandIonos/res/values/ionos-setup.xml b/app/src/brandIonos/res/values/ionos-setup.xml new file mode 100644 index 000000000000..615d16acaff8 --- /dev/null +++ b/app/src/brandIonos/res/values/ionos-setup.xml @@ -0,0 +1,68 @@ + + + + + IONOS HiDrive Next + com.ionos.hidrivenext + com.ionos.hidrivenext.provider + com.ionos.hidrivenext.providers.UsersAndGroupsSearchProvider + com.ionos.hidrivenext.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH + com.ionos.hidrivenext.documents + com.ionos.hidrivenext.files + com.ionos.hidrivenext.providers.imageCache + ionos.db + ionos + ionos + IONOS HiDrive Next + Mozilla/5.0 (Android) IONOS HiDrive Next/%1$s%2$s + false + + + false + https://storage.ionos.fr/login/v2 + https://storage.ionos.fr/login/v2 + + + false + + + true + false + false + false + + + false + true + true + true + true + true + true + https://wl.hidrive.com/easy/0121 + https://wl.hidrive.com/easy/0001 + + https://wl.hidrive.com/easy/0102 + https://wl.hidrive.com/easy/0162 + https://wl.hidrive.com/easy/0182 + + + https://push-notifications.nextcloud.com + + + 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com + https://nextcloud-a7dea.firebaseio.com + 829118773643 + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + 1:829118773643:android:512449826e931d0e + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + nextcloud-a7dea.appspot.com + nextcloud-a7dea + + + diff --git a/app/src/brandStrato/assets/branding/config.json b/app/src/brandStrato/assets/branding/config.json new file mode 100644 index 000000000000..9b2ea16654e2 --- /dev/null +++ b/app/src/brandStrato/assets/branding/config.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "loginUrls": { + "byCountry": { + "DE": "https://storage.ionos.de/index.php/login/v2", + "GB": "https://storage.ionos.co.uk/index.php/login/v2", + "ES": "https://storage.ionos.es/index.php/login/v2", + "FR": "https://storage.ionos.fr/index.php/login/v2", + "IT": "https://storage.ionos.it/index.php/login/v2" + }, + "byLanguage": { + "de": "https://storage.ionos.de/index.php/login/v2", + "en": "https://storage.ionos.co.uk/index.php/login/v2", + "es": "https://storage.ionos.es/index.php/login/v2", + "fr": "https://storage.ionos.fr/index.php/login/v2", + "it": "https://storage.ionos.it/index.php/login/v2" + }, + "default": "https://storage.ionos.co.uk/index.php/login/v2" + } +} \ No newline at end of file diff --git a/app/src/brandStrato/res/color/radio_button_color.xml b/app/src/brandStrato/res/color/radio_button_color.xml new file mode 100644 index 000000000000..1d8ce6d247ea --- /dev/null +++ b/app/src/brandStrato/res/color/radio_button_color.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/brandStrato/res/drawable/ic_launcher_foreground.xml b/app/src/brandStrato/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000000..93b869a1366c --- /dev/null +++ b/app/src/brandStrato/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/src/brandStrato/res/drawable/ic_launcher_monochrome.xml b/app/src/brandStrato/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000000..9376b708bc93 --- /dev/null +++ b/app/src/brandStrato/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/app/src/brandStrato/res/drawable/ionos_logo.xml b/app/src/brandStrato/res/drawable/ionos_logo.xml new file mode 100644 index 000000000000..47d6dafe700d --- /dev/null +++ b/app/src/brandStrato/res/drawable/ionos_logo.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandStrato/res/drawable/logo.xml b/app/src/brandStrato/res/drawable/logo.xml new file mode 100644 index 000000000000..47d6dafe700d --- /dev/null +++ b/app/src/brandStrato/res/drawable/logo.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandStrato/res/drawable/nextcloud_logo.xml b/app/src/brandStrato/res/drawable/nextcloud_logo.xml new file mode 100644 index 000000000000..47d6dafe700d --- /dev/null +++ b/app/src/brandStrato/res/drawable/nextcloud_logo.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..ac1451533e33 --- /dev/null +++ b/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000000..ac1451533e33 --- /dev/null +++ b/app/src/brandStrato/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/brandStrato/res/values-night/strato-colors.xml b/app/src/brandStrato/res/values-night/strato-colors.xml new file mode 100644 index 000000000000..ffc5f64f847d --- /dev/null +++ b/app/src/brandStrato/res/values-night/strato-colors.xml @@ -0,0 +1,139 @@ + + + + + @color/ionos_default_text_color + @color/strato_blue_400 + @color/strato_slate_900 + @color/ionos_toolbar_color + @color/strato_slate_600 + @color/strato_slate_600 + @color/ionos_default_icon_color + + @color/strato_slate_700 + + @color/ionos_default_text_color + @color/strato_slate_800 + @color/strato_slate_800 + @color/strato_slate_800 + @color/strato_slate_500 + @color/strato_slate_600 + + @color/strato_slate_900 + @color/strato_blue_100 + @color/strato_blue_100 + @color/strato_blue_400 + @color/strato_orange + @color/strato_orange_dark + + @color/strato_slate_500 + + @color/strato_blue_500 + + @color/strato_accent_dark + @color/strato_accent_dark + @color/strato_accent_dark + + @color/strato_slate_600 + @color/strato_blue_400 + @color/strato_blue_400 + + @color/malachite + @color/navy + @color/madison + + @color/strato_slate_500 + @color/strato_blue_400 + + @color/strato_slate_800 + @color/strato_blue_400 + + @color/white + @color/strato_accent_dark + @color/strato_accent_dark_pressed + @color/strato_blue_600 + + @color/strato_accent_dark + @color/strato_blue_100 + @color/strato_blue_100 + @color/strato_accent_dark + @color/strato_blue_100 + @color/strato_blue_400 + @color/strato_slate_500 + + @color/strato_slate_600 + @color/strato_slate_500 + @color/strato_blue_100 + @color/strato_accent_dark + @color/strato_slate_500 + @color/strato_slate_500 + @color/strato_blue_400 + + @color/ionos_default_text_color + + @color/bg_default + @color/bg_default + @color/transparent + @color/appbar + + @color/strato_blue_400 + @color/strato_accent_dark + @color/slate_grey + @color/strato_blue_500 + @color/strato_blue_100 + @color/strato_accent_dark + + @color/strato_slate_600 + @color/strato_accent_dark + + + @color/strato_slate_600 + @color/strato_blue_400 + @color/strato_blue_400 + + @color/strato_accent_dark + @color/white + + @color/strato_accent_dark + + @color/strato_accent_dark + @color/white + + @color/strato_blue_500 + @color/strato_blue_100 + @color/strato_blue_500 + @color/strato_blue_400 + + + @color/strato_accent_dark + @color/strato_accent_dark_pressed + @color/strato_slate_600 + @color/strato_blue_500 + @color/strato_blue_100 + @color/strato_blue_100 + @color/strato_blue_600 + @color/strato_blue_100 + @color/strato_blue_600 + @color/strato_blue_100 + @color/strato_slate_900 + @color/strato_link_dark + @color/strato_accent_dark_pressed + @color/strato_blue_600 + @color/strato_accent_dark + @color/strato_slate_800 + @color/strato_blue_400 + @color/strato_slate_500 + @color/strato_slate_600 + @color/strato_slate_700 + + @color/translucent_strato_slate_900 + + @color/strato_blue_400 + @color/strato_accent_dark + @color/strato_blue_100 + diff --git a/app/src/brandStrato/res/values/strato-colors-palette.xml b/app/src/brandStrato/res/values/strato-colors-palette.xml new file mode 100644 index 000000000000..0d38616cd2a3 --- /dev/null +++ b/app/src/brandStrato/res/values/strato-colors-palette.xml @@ -0,0 +1,45 @@ + + + + + #15CF77 + + #F7F7F9 + #EDEEF3 + #E4E4ED + #CDD5E3 + #8493B3 + #5A6782 + #2944CC + #272CB2 + #2F2F70 + #29294D + + #FF8800 + #FF5C00 + + @color/strato_blue_800 + @color/strato_blue_900 + + + #2B313D + #333A47 + #3A4150 + #454C5B + #5A6274 + + #4C84E8 + #6E9DEE + #7EA6E8 + + #D92B313D + diff --git a/app/src/brandStrato/res/values/strato-colors.xml b/app/src/brandStrato/res/values/strato-colors.xml new file mode 100644 index 000000000000..39a05445d05a --- /dev/null +++ b/app/src/brandStrato/res/values/strato-colors.xml @@ -0,0 +1,149 @@ + + + + + @color/strato_orange + + @color/ionos_default_text_color + @color/strato_blue_600 + @color/white + @color/ionos_toolbar_color + @color/strato_blue_100 + @color/strato_blue_300 + @color/ionos_default_icon_color + + @color/bg_default + @color/bg_default + + @color/strato_blue_100 + + @color/strato_blue_900 + @color/ionos_navigation_bar_background + @color/navigation_bar_background_color + @color/strato_blue_100 + @color/strato_blue_300 + @color/strato_blue_200 + + @color/ionos_navigation_bar_background + @color/strato_blue_100 + @color/strato_brand_text_color + @color/strato_blue_1000 + @color/strato_blue_600 + @color/strato_orange + @color/strato_orange_dark + + @color/strato_blue_300 + + @color/strato_blue_600 + + @color/strato_blue_400 + @color/strato_blue_400 + @color/strato_blue_500 + @color/white + @color/strato_brand_color + @color/strato_brand_color + + @color/bg_default + + @color/bg_default + @color/text_color + @color/text_color + @color/strato_blue_400 + @color/strato_brand_color + + @color/strato_blue_100 + @color/strato_blue_600 + @color/strato_blue_600 + + @color/malachite + @color/strato_brand_text_color + @color/strato_brand_color + + @color/strato_blue_400 + @color/strato_brand_text_color + + @color/white + @color/strato_blue_600 + + @color/white + @color/strato_brand_color + @color/royal_blue + @color/strato_blue_500 + + @color/strato_blue_1000 + @color/strato_brand_color + @color/strato_blue_100 + @color/strato_brand_color + @color/strato_blue_1000 + @color/strato_blue_600 + @color/strato_blue_300 + + @color/white + @color/strato_blue_300 + @color/strato_brand_color + @color/strato_brand_color + @color/strato_blue_400 + @color/strato_blue_400 + @color/strato_brand_color + + @color/ionos_default_text_color + + @color/strato_brand_text_color + @color/strato_brand_color + @color/strato_blue_600 + @color/strato_blue_600 + @color/strato_brand_text_color + @color/strato_blue_300 + + @color/strato_blue_300 + @color/strato_brand_color + + + @color/strato_blue_100 + @color/strato_blue_600 + @color/strato_blue_600 + + + @color/strato_blue_100 + + @color/strato_brand_color + @color/white + + @color/strato_brand_color + @color/white + + @color/semitransparent_black + + @color/strato_blue_900 + @color/strato_blue_600 + @color/strato_blue_600 + @color/strato_blue_600 + + + @color/strato_brand_color + @color/strato_brand_color + @color/strato_brand_color + @color/strato_brand_color + @color/strato_brand_color + @color/strato_blue_100 + @color/strato_blue_600 + @color/strato_blue_400 + @color/strato_blue_400 + @color/strato_blue_100 + @color/strato_blue_500 + @color/strato_blue_400 + @color/strato_blue_500 + @color/strato_blue_500 + @color/strato_blue_500 + + @color/translucent_white + + @color/strato_brand_color + @color/strato_blue_400 + @color/strato_blue_1000 + diff --git a/app/src/brandStrato/res/values/strato-setup.xml b/app/src/brandStrato/res/values/strato-setup.xml new file mode 100644 index 000000000000..23c694767143 --- /dev/null +++ b/app/src/brandStrato/res/values/strato-setup.xml @@ -0,0 +1,78 @@ + + + + + + HiDrive Next + com.strato.hidrivenext + com.strato.hidrivenext.provider + com.strato.hidrivenext.providers.UsersAndGroupsSearchProvider + com.strato.hidrivenext.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH + com.strato.hidrivenext.documents + com.strato.hidrivenext.files + com.strato.hidrivenext.providers.imageCache + hidrivenext.db + hidrivenext + hidrivenext + HiDrive Next + Mozilla/5.0 (Android) HiDrive Next/%1$s%2$s + false + + + false + https://login.hidrive.example/login/v2 + https://login.hidrive.example/login/v2 + + + false + + + true + false + false + false + true + + + false + true + true + true + true + true + true + https://help.hidrive.example/help + https://app.hidrive.example/download + + https://legal.hidrive.example/imprint + https://legal.hidrive.example/privacy + https://sources.hidrive.example/android + + + + https://push-notifications.nextcloud.com + + + 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com + https://nextcloud-a7dea.firebaseio.com + 829118773643 + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + 1:829118773643:android:512449826e931d0e + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + nextcloud-a7dea.appspot.com + nextcloud-a7dea + diff --git a/app/src/generic/java/com/ionos/analytics/FirebaseAnalyticsManager.kt b/app/src/generic/java/com/ionos/analytics/FirebaseAnalyticsManager.kt new file mode 100644 index 000000000000..55e9d5dfa405 --- /dev/null +++ b/app/src/generic/java/com/ionos/analytics/FirebaseAnalyticsManager.kt @@ -0,0 +1,12 @@ +package com.ionos.analytics + +import android.content.Context +import javax.inject.Inject + +//STUB +class FirebaseAnalyticsManager @Inject constructor( + private val context: Context, +) : AnalyticsManager { + override fun setEnabled(enabled: Boolean) { + } +} \ No newline at end of file diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml index 2e8c720f49cc..8f7227dcb476 100644 --- a/app/src/gplay/AndroidManifest.xml +++ b/app/src/gplay/AndroidManifest.xml @@ -31,13 +31,13 @@ + tools:node="remove" /> + tools:node="remove" /> + tools:node="remove" /> + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/ic_launcher-playstore.png b/app/src/gplay/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..96266b96c3de9858e6a467afd882caa57fc4be5c GIT binary patch literal 14102 zcmeHu`9G9x-~TaVFp@1v2$Lm|CD}q4+t>$`T^$j1-k!mI@(6rBsA$qZHX0 ziflucvG2Pv^BlVBx}W<`_ad3 zXeRh;@mBjf0Lr~adfF#%*-rI(`+xtwSu;nbUpaJ{8?$#KB%P3kcKNdY>TB28EOzvr za);gT9d@9PqZoJQa`WhK6S6Eetbw&U>hw(ae1OWE}Q1B4adhkc3&i{S<|6C6K`^TQmkrpXg zq${2FvC2NLGR^$7Ne+|I;g98vt8LVSy*3Tj!&724140W@OOAC#_~9%2YUbJd_ueX5jv{|Flk|DoIXNNnw=c`a-6A7z$VKjmf; z?y%wx`GM6v2yl;3eBR-)|D2wYR!FLv z*d{ip%9)hDGpk?IvXPn-4B^MSftJSs|5-g^9xEyM$Go0#S`*-65qhM+AWv4K($SemV?pxLu|Flo3WX^ znsUcn&h3L&*{x*_VzdExdB86%nNBpx?Cm_(^wHO*jbm!a{Me_rnHwTqX5F&R23j&p zzDM1zoCr@<1s$A#;9k!yltG_#LH?`9W)6QtGrer@Z?=rmuO4=% z08SPZNIUeY)p4`_xk+7rX!6+A@;#xFf!!1e`hs z>5{s_H{Nu75A)Gcmnt?(WVWc$nn=bNmbt~vsUyw3Q2>)qefBiOvRC{yv$LDaNSo7G ziz!P?b`sNx8ZFVrz~eYh@HrfSiLrac52j0I-KI4&$A7ZgKJ>~+jlH50Z)}m+T0O&w zZR7&yU;FZ!#h&(l<6+xiZ=b5=$mytlHA8Xj%sa6iD1zEvP^mWO!$_Jh+z`7txlb)n znACW?DZZ*oI1y*rJkk!#O;Er}R92m=T0>UDYY5_{(M&;$pQlLA=u$7p*Iv$TFu8f5 zz?X{LpxAcjb8j=OWX^cl$*b>JePUalE5YO`XP`sj8krHsW}(47r#TcZo49*Ps32icP`R>0 zZF_umhfH*Vk@cf_gRCu2dCipja6Qre9A^#|5c0&mg!VSNoaNdxTz*|~z4EbV@cHTn z-j534a0CE$Bqf{jZH2>v*TkYWHjdk5yK=P;KP%qj!g;3`4Q2%?_sCzlvl?e>8kvN0 zLVBKUl5(1lP`(&CZX&O1!^KCh5ZB9Dv!+HRB5;B^EGeoahY)Tx4wZ4f^9)GfFRE#d zIp(t8-FAl6xgqGRq`H)|f#zP8lww8nGSnzwbd3|#%4svUKEM9K-z#Da*6$y?U^WK;k)>JhiAL74`>3PpP_3#U; z0sj)`J$)3o)d`%G9D|1v_9*gSt(4(c{;0&Mi~*JTU6DQeeXg=ur=)Lyn|l-st`~KY5dogIUFpn-wbr3Tjn><~wH&uORGX85 zm4>UGILpz8`KW(=HP|3dFh0E0i%GZgQpOu(paCE_6ckTs)}@hUP%yw) zCSvZ7JxleC7dw}x!t`Wm_bl|wXU3_E$5IPZ1_NgqZJw#oLqzVgZdV>Z)UN{HkLa6E zRGw2+!>=0uDARz-I+vlz5U2~V?W5IF@UO?$7kM)W;`77R*j8B(;COlx4O!12z;>XY zxx`M~dEPY(T6jbW+bX<0SI3sFZ}WIjtC|%n3;AcUYpl29+dV2Lpz-FcmhFv|x0guK zx~Q{E)gTRph^Wf>r5*1;u{{gA}&cOTO+l2dQ@fSh>s={pF3l*fp9^{v7HecA8X_{3>@)eY^aGR{wDn zD3-0LGB@Izzt`%Bd?)7dL)MAb>WxKL-JVX#bNtrd5yELvJ2(C>NvH z5j$6fKpwPD9^XFl&!uKo{M%zc)tdx(VffokX6zS7fjf3TfWx@fwE@li8z|}M{6!fj zpBjFO^}z~`N>MaGtEa5H`fTuL97kP`wJo;cUwnGSUzlLY2{=#t4wzNFaxgjnb8m(= zD%Zm623^tL$VV9)UTZm;5!)WWjn`se%{N1y92&xS2|C z`-&T6?iVyleWx2+8kLyoGT3x*xuzsCCQ~*;eC~5h?5$5|2Zh%aGHJY5?*_VJ55o<2 zE%_abUv%n@2;rzyZ+dU#GKZ*@>B5H;i8<6J#g?zK)t)Ifdp|N26Mm*t%z1v7k>E;r ze&X}B*RL{Zm)lI4@3cmKId9k^es>icls}e`5t1W=1mi~q()X!IEAeZ90bZ-X{kwM* z8ZX$rjtjADj?E0KW@HFz+P(c@anGmf6_%5C0YU9ryTh9yl!1IY8xL3+j%U=>9_x-( zqANL6W2*?Vf?p`|x`cdDkY<|PiYLaN7xip+d<67DWmPXV{FIRiEI$h!S9LXoU1WPD z7GP6GyYS;*DB(s2u)SY?U=?M4p;9_-#^(A=?KLwZcX+t^-q z|9c?>_;7lL^rGohb~@X35XBneMY*I?t4*1Xgzy(JBk(Q?5YAqk&%ekbt#w^l7WnH1 zW6%H8-b#qBsE|b|@AS^IA;DGsD1ou5E9ns|fKI}sN%&fXXiX=Jp*N?wtkEEc$7Z9$ z>M{iPoP%P6 zV`1=7oFUfvJj4_@6NsF^T+}ziDy(~2g`Wv5e>*cDvao8|mXx5*!F{)odzKkqA#*^< zLp)%eJwqFufBZfpw!B}-NoCx~>DJey%G=gC5kQGVO^Dxu?d#itl~H%_R;Jn(D85H< zkq-%89<$@UrZlUafdGVDpFFiBlz^>Ly`y^nF%&pyU3G25aJ_&xoI@DIH8D8~v@TX( zj%~T|s5PW=OFo6WHZA2hIr@`5m4650z!@i;I&uaB($4;JXEvJ976|7M1X2&J3&=jw zxi`pbI6tK8*0+oE!nQO9b8g?DXcx}wq`Xc(y*q^m?#aUQKrk{9_xof!$iPhSsC_~- zc+%@%Bz9BKzCh-l5d2=hV`1@UOAChpcq!^IH_;?1l5%XjvmK`(3M8L6XIhcDlUlu+ zhDh7z9=2lA9V4JBHOjU1)mE0(HT1f=W;;-dFQUH9`Wo0_?$j^ zHX>Qv)i~qY$owPoot(h!G(RR&a6UH zQa?}VVf|0;egSA*r2a#1*dxV|py}mJn(s6{lrpMDFWIzHYonctZ?4b~&+>~halL}{ z*NtLPTS2|u!?`6oo(cLazu@;gt<@}lo7!($1$#E8UNPQh< zi-DQvAx%>Ze*puTbCUW>VV*O&K{BhWF+?$#x5sk!i$Du#UoByb zz0n~%ScMXKT@x@V z{fjQi4KZT%EQ?Of)D3p6rRD~}lbI&7BW$JvI#y>Y{T8b$eao|JeSR`TEVJA+fN!1c z`)KMOwk#kl&MC>8d!%=*;{x2LLVYbbZfZkEjjW5F4elXT#WvIe7);Ewl&gf|%SfWt z%q)6!&%MpQkxe%_yFxoYjuJgu>r(~j8n;ehI@!OMUW#qSrMo~Mvgkf;W*mBG!5$?t z@N=bSjC!E17Qbmd{nNW=b>?%@R`GbBH}X)IWIM#X_y_g;n5k+vBxrp%9Gqt}MLn=> zZ>5%}7NPGbf1<)S$2@a&=(kd`5_Vk?0bQ$TM3M96+p-0^x1;NnMJs1oJz|n&Rx==> z?9*^t#PYTeFcHauuFA|%xo?mG)l;yx$vuyk5bapA+{w15L_WF=(U+(>b$5Q^_5r6N zbA{h)AGESy0rxP1YX%t$dHa3U2!6;iIoGdTlzZ+vHR-t0yH6mnSs3-!m~7NqM?`VB z>3)5JFHoaDu()9cRG56Nn_^Wm2TtXOd$B5Q?ayyVv^wN4qpW!Z(<9}zVNklYs(qTR zKtng8FG2ZoQv1v)#{!!iO(c-|4$FClK0JQcc6`InOWf<}h!5!}MmZSO>3Q@4 z;vY!(jUz_r+2o!qS0jk#WxK6(QGgoSM@zi*$P-k})~SOX!c>cjxdG_8 z{{FvGE#6ahDq%22m#^4=aSh1hCPg5Dy*vFD$ zu-jQOFdXz>U`eUVUbF7=BIXA{2kXY^GnZivXkBaF2dC^#wMGO%-+N3LtxsNxRjde% zX?^~y1`UKVCpOsLl$o4ev=e`vZ+A#arC(fazQYMG05zo_g&x;HoZGe2M$P19eP+7z zfvtV^!gj}D%2E>feXI?2ma%s#Pk?s)?1GkMYyIuTYU~x{vsb;H+2Ui*Zs63z=px{j z_A-_-d9^LcIOWBG=j#K^B?~rD>pzcd9qL-}k|*Fm>qk9(K(cSe`GwIjK<~`2yBV3E zPI((uFA926XW1wrL!JlaopM8>|K7Y*{|zUo*S=8hsb*7EoFSoiwQ z>c+y{A-9ZRFt%Q3J0lRLLcLo$dgAk+IQ8t=W;saQuRWNW5gKyaRzV;#e zkW_ZrE`@ova=smkx4%Q!-(razbZ6#smPZ3F#mX}?W+}6Ku5Hc=P8BPo)bTjl2f6yA1fTU@j0hXf9OdYkdiPBCCIK+T%(o^L>6W| zRqVRL3C?9rm`3DTf4i$Wo5gzi=`@vZEwtRy?n^NEyc_wG0sg4nq@&9c`0LRe=**7(r_+I8M?>c=?`gVoD&T@sBhA8Q=5oehAS zHiUN4eB=k5IEwn~=gKHm^tdT)g6!U0F<9L!Jka6NvoGUyp&$a&P_W?IO1Je|uPQ3^ zpH)i1qgeN14q4P_*cWsZih?^a6~8)uCB(;C`gZ2{6NIn-2~TqRH$FI`TZX|hs#v1j ztyIkJNUdX$GY+61!cs8Vl8UTn=i}j+4Q`xCji##`QG{PV`(&cR2F zZs(##G`Nsfx+$<^Q#lnlDLQK1?lj^yoE4B*L8apBC6^j}Yjdv?I8IJ3XD_UrZWUly z>N?-Yj?RPqop9xzE3g&j=(e`K_R+_OVS)YXzO@__YPrqg6cFOvmbZMO(8xbD9cS%w z0Ev|mw^1~Oai-yh)~e~(r3z14pB#@F%ZjZD8XY>h=9g(i+>xPC#DhrN^$W>J8iJ{I z{?M_E+X&m|92;)sdBg`H1X6j;@cd2T-9)qYqp$^3{VN2WI~z7AIjy|^9gZ>zm_Sqlc#2aLDkO$D`f zPEv_9<`g2iKAW5zd)Y6tKK>5jtVz=Q?~dKh`XDC==GPBlD~6wE+pN^o{vTUm2xd-rQkTU9EMC3^R26v9}8yuP#Y?#r+8sC(VAq zv;r1n;e~xT6u~2hY;8cd4mU>KI!0#ujlwuuArBiun)#$z(=V0GHJMj`o^))*fAH`- z=_rmY(=(v05VP87;Z$cCo_fUf?MF)I9fE`vx_kw=K+`kZX33_7m~<+=TYn0{! zEqxWH@iA}(`EsO(>F_dNM){Upq|Pv8ey?1D^MuYEEWz@3Y~ouxA@x9rs~wuUbdLKv z!MbaR{KUBe%Hx#bxY}up8ka*-9Q`~c-F7O}cOzyjGQMY``*wdjN-)Y}aIsr0YaQvn z2h8t|sE}oTvZ}6dxRM0PzU@&?c;i$0d2B!05JxbVj8kd1|9S5ID!XoG?`U?oF~hAO zk18@EW!YiKOx3Sd1F`-9W5zMDxFiu2nQnIF0`bx4Yh)4Al$tBwu#D&edW z{nQ<@y?*C>6}V@GQ3Q7sr~lad*%Jb8D5h8k{?bZUw=)K;Ee0uI^@0f8+$f`y%&pTG<+tf@`kK&w77#j z8mbo5arv>rx+c#49)^QXVXezhNNyp;lR>d+v@;o>L2HZi_tN=dFPg*{RdPV)FMhX| zTJc(W8Fq#DI2O>AML=@;!1sLd1z9h;=(?LaQ(YW#tNBWkX2fJ)%c`c07lGpX3SMKRD{u>G;sXrk%#92O+?Q{oWGPFFV$sGJq34FN&;m z8lIqUY(4sUWX0?oJtcD?Td&9we2)lv=dYHw+3xIayNu1?c*}+8R}5HJ$y>a4{0t}> z`l;7E#dr1sP&t(g5&lHkNvUa;dmkV& z@4%E#h!<=^425$L8hxu#R);Kpaj{4%MeA#rzU%h(Yr*zoQv__Swv)o7JlqvNxqt5S z@usGh7WkyL+}c30)>jT#Q{2m9zO&WHzCt|fw1_Aa0VoLHO{xO?XV3Jz3eNZ#$X3i~ ztSvpnM{`-zhWJj{t>z!upmoguG9df*NkBY2p>@!JYx>#1%>5!_Y^hh^h>z~mH7i+` zKm)-8Yt>YogD-b!r1Cnj<^s2$EFOrNyz2)Kizf*-YznQFAN8d*ADa5weCfCw>co5( z@6&g`mWbb87F8_Xt*W8#rBj~*&go}z>Zas52ZxAW4mGlUphMu$5oDZFAcBZ(q>YqJC=LNu;I%M}Hv4{|-S za8}9B!$eDWNZ4?t2Ja-|D8jIL=e-SqnSVbrb0fh|w;P`XuS&|EIVi?(Oz%WuFOBh3PE{XqF4%-A$E6*BI$g)C8GXN- ze%;eHNBy65&Xmx|u$={cTOEnrhp3!+6F93Y*)#X}e=U=B?AGZWw~(D!@a+_1=gDR>#eZnMCY zCK^N*)LMUJX@N=mG)(zUpg~NV*B9=ibju*X3CpvGO-c~vnQ2(y#(}6z(E<0udwy+_ z=9@m?lA>VlnwH?5`w*6YeDVMF%<_pMqu>Kqb~|g^nZ49OKlo2EWY`nwQrD!2HGsVk)B=;LQZ2=84#5k zzcpJK;Jf5%QFH_67X_;V)YoAi!xACJ+{_)HHvYZRblm|Bk=N7b9150J-?mn3F%iR>!!HahB^pXUUAj-@quzir2@YusvSqf8j_uq4?J3Yi7 zfP>$L2>b;d_|TUo(ac8l6-3#;er)dHd*~G;QW(K;Z|ms{FEpUEWZK=o)ads;Cwvw* z0sb*vY~$G6B&<)umH9syzzh*iZnCECNrcy-VH5n1tlN*)y){Uw>C)+TXlFyD@q2bj zdi*fKW!+JM-uMrkyvz$2@G>fK+%rLw#ekE+6^FiGtz>{D(wCg%d;j_gcIgr<$s*R9 zD1};yP)09Wzk7%Ubg|+X^%ws|rY0G4U#ll8c3*Y1WT8T~I^v4-%a4b4O{qvpJ^L4k z+`N41IZWzWLdU2!IGCxgCz{5$ziZwd&(o^^7j6!h!e-{>E-fS)D0$Luva0SHmH*cW zUuGTaoNOo#DJ(~8!D>R>bGK9@{(}WO5(2A~CJWHa z=*RR|r~NgH!{3)rq$Y0hVy0xEIC8RZ0VBpgs2$-C!oP|Bl_53Ji#B$*TYT;|7fzkk zn+seu>d1l6X84}B+~uSr?BM@3lH|VEq`9-{qq~43zgj zSbDg3&8&CdjiVa|P$!YGen;cH?4No)f2VWcut@MfhVpe8q*18dX6%tvIP&Z|smBfr z$8j4qeHg-`*^db9;rSyCWo82Q?SiX{*TdFGk$G5xS`a%<)}F0=Ug`Kp5bu=0P`pq= z`~7BEl^H+^MviTBZEPv5?nqTOe^&XyNPyED>&1iuAz0M8i1@;VPS3;|#TMPpA{hRG zqjxgkQe%LA6Mf7gqg>CE3w(AVyF8C&+iFwU*WdDtyJrgshP@u>WJ*u3bjuavMbARz43|LssWc@sr zRW?ewfdVQ{=utHsX{PJ4KL#!Qj}OFIu>)rV4M{k^Xj$6;h)U;AoY9?^3m`4()2cbA z(4Wh_+YPposga`GIOr~iM=0E|_hJNTD~jbeR8^Qt{@oIZt&29ugf-5N)SAY?Kp6DW zR?Co1o=evuz8}=Yz%KZ2RU7A`2;q;_Q&!BA6RGcu+sLl4J@Y&d{e$TFAoEz)uOlH` z>)Pofe-M!>1QtK%j}XsF*7BNpGAXA|?V&>K6tC@-Fl z%qb}INKq`l_*~3$&y|8}v%IjuG-hJO_U6&vscH?;pvygc=GK3%7sv{H4n!U<-Dswh zzrwk#q4L^SG?KtPs(2PQCLn(x@z(LTyZ#<#2k`uodT3EzmA?M9DfjX@c7u(JEQm_c zU+P@M2-cPw14~_*G;|1 zji|KWp%xVly@u(;ifGfvt`-jNeSdUAq;2}~%}>_gY$EqfEq-Wpf)jA3`Fs(h@#lhC z>Kpcah{_Dbn<}t(2)n3{hNJ^~SFPsk{@kgiLS5YQ%|>NTeQTQc!_i<3-LR8kjzpb6Naf`y&2>aqG8ptOkU9I!o(BP_sfOlPLg{o8gpdm^08=v=7?2q6d zzu5qAI6Jl|qX%YL${jOqkQpHv=;~Ibwm>>PYT$g}_qhk;$SG89*exLC)VQ>nYF+Dl z6>Bm3R*|?iectbo-Ht_Ms&SV}fSB{= zE4ADA`FA4`TUJJxjTgSHRc%I86=(IryR;9uwanPc?P_Bu_U1@rLG}@BAVV&Z7-T^_ zU%h|k9oAJVwl7gWdck6y1YHI3IgF)+R*)yK-CEv&*Y}o zwn!?aeCN|9JM|4%kvIqvVeCkrDZe>e+VNO>YW60#yr5jz%5 z(Er8!25gPwdZzvJ*QW{vX~mG|Vr9XkCH8vS>HusXj_wguuE3}9qk%F@$n7t0GP3%_ zA0{8yskbBZZf7FKz;NKfpHL`DJ8-id_wmcEV9#ZNdWNPet|TSoK3VNZOty5SqXeAAKZYn7urL7rsKEvpi8MNdu*c!`6vIYrc> zAklWjc{8Q8A8GscBANHl;8+Gpf$nnO{?hh#Gy*t$Pir$DQ=itpg6voATH|#&BBC+9 zKmOL6T*j+#{^&u9`F<+nM_rk71FBNpbBH`uco`SKXbuyOlP2Yny2-SvIO}t+6=Y402wEuG>~o9@GJO1QOz`7ug2)eQkeUcc!xgla z`*&VFQ!p5XtJ+@dJpF?sIE8MJVT;UBg;V?7!0f#whX%DXWefHhK}K-Lh? zn}NH254`YFf-dJOpJ;JjfuxE1PuYtv!tDzw9vK5n<5z2&%*1B%eRiZCqj21T^pu;= zMf2Yj?NXm8NM}22Zv9n`ex}P#y(-5Qd=3VLw}y2B(!3m27ZiVCogE)Fl)3C{qY=U& zb0ukrosqN|X(8ELe-vDG9ebl?RQhV$7!{Go1*tUmKdzDHp5K*bPCK1I^g7yQtE&IY zYJvG2oTPSspRlTQ2Um}((-$igR4E*KP(K%V6His_X;~W)w9^5ryYX5r(PP?6Fw?s!``BS_&u;Z z{LpqjgF+pn2EjB0BeLj85aQZCg*q=$-e>8$%KBOYh=c;dy$h;fy|g1VV9^EtW`z-E zMudALI8kvv=*rw_l)Cuca`VwL^Rp=U=eBA)Va{JX+NGo4h$eKA3{7y49kRZ3?6}&q z?<7vHe;M7+X?=QfQ|HQcUjx~)%aRl-f&)^ku0jQ6S$-;ie~VIs%MR+4zqa7_sVOx^ zt6vEc!%peAR4hW{-ZizF(R!nj>{&$)&yK45>iydS$rXrN^bj@2~p7}^5JT8iV42UgF_^uPK_2Qpm@S`)_1QnhI3{Da(&)>@uV7uhxg zz^nvhLmCNlk}Od^#+?Br1Fn6AWRpaiKD@w1XqgpRurn`JY?-j7-vUlewRw)kBv&dG z@q3?y=!bHN?6#8PdHz>C&*hXp>rFKd72th4!tn)a&fm<8`JDXq57ONWA6tXcmlGzb zdPuD#iMgN9u%$%+X5T3K!^G>aO};lB4MLtA2wZC>(YpAmoB6Pl&}6TO4YMVLG3Ops zQ)mwl$!VCla%hU<%V9`9HFgh0js+ULh?~B=FHJdNdi`NR>C5OQ;kN(*y|N5TQ9YA=r^S^v=V1W3?_XsvyeA^Ov zb6u=scBp&F(OexU+GgE_` z<*^k`O@RQCrQMQ!moNcx1dY2`#u;=9%S3WS>9U();i(M357Lrl0`$#Oci6t4;LfND z=Q*$(xK}~BT$H!(eVhjk1l|GQmH5!mZ`hn`ii&7b*aczR_ATtDa#ZR8!qN`+g{D=7_lnrOy}C2qmBmEw%{)eS`DV>GDbi&fucNS>Rx3@wUJ@KwiHhQVC{4wGhT zDc>5yVQ6=pI?z6puD)*gIUm$o>l<&nVHWT!+LIWddW{BNLgM8~9rf0VL7_Mw>vApb zv=n+lc3*{?=L7nn#l`_826e=Nta^yTz;M7A1Vj zGRLL|L*>UUQhEz#|Ml&QE$-OAen;qn|AX+oUz$1GuZ3Fy!9$@g^?3~0y;HfQZ(ndnBu_+lMl_)C!p(53GITS;?T!XjdCfPPD3_MMwm&+CLfvtdvSD;8xYU9Jz>uPu~4X<$Fnds8* zxVM}x=zDA@k?P=tpV~f1<^TimKUY5Qmvnu5?#TJOtuHJSLqh*?y~>-AR^Je-zSD3; z-8jqBwebq_Kdy(>;{v&Z*M_PQ6fpz)0Uz?~Ts++y4VEhaijq literal 0 HcmV?d00001 diff --git a/app/src/gplay/java/com/ionos/analytics/FirebaseAnalyticsManager.kt b/app/src/gplay/java/com/ionos/analytics/FirebaseAnalyticsManager.kt new file mode 100644 index 000000000000..5dd4233aa3ac --- /dev/null +++ b/app/src/gplay/java/com/ionos/analytics/FirebaseAnalyticsManager.kt @@ -0,0 +1,23 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2026 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.analytics + +import android.content.Context +import com.google.firebase.analytics.FirebaseAnalytics +import javax.inject.Inject + +class FirebaseAnalyticsManager @Inject constructor( + private val context: Context, +) : AnalyticsManager { + + private val firebaseAnalytics by lazy { FirebaseAnalytics.getInstance(context) } + + override fun setEnabled(enabled: Boolean) { + firebaseAnalytics.setAnalyticsCollectionEnabled(enabled) + } +} \ No newline at end of file diff --git a/app/src/gplay/res/animator/progress_bar_login_indeterminate.xml b/app/src/gplay/res/animator/progress_bar_login_indeterminate.xml new file mode 100644 index 000000000000..fd660c547af9 --- /dev/null +++ b/app/src/gplay/res/animator/progress_bar_login_indeterminate.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/gplay/res/color-night/drawer_item_shape_color.xml b/app/src/gplay/res/color-night/drawer_item_shape_color.xml new file mode 100644 index 000000000000..a4d31dfa8a72 --- /dev/null +++ b/app/src/gplay/res/color-night/drawer_item_shape_color.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/color/drawer_item_shape_color.xml b/app/src/gplay/res/color/drawer_item_shape_color.xml new file mode 100644 index 000000000000..a4d31dfa8a72 --- /dev/null +++ b/app/src/gplay/res/color/drawer_item_shape_color.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/drawable-night/favorite.xml b/app/src/gplay/res/drawable-night/favorite.xml new file mode 100644 index 000000000000..f2728f1758d5 --- /dev/null +++ b/app/src/gplay/res/drawable-night/favorite.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/gplay/res/drawable-night/file_image.xml b/app/src/gplay/res/drawable-night/file_image.xml new file mode 100644 index 000000000000..5b205f0cad09 --- /dev/null +++ b/app/src/gplay/res/drawable-night/file_image.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/gplay/res/drawable-night/file_movie.xml b/app/src/gplay/res/drawable-night/file_movie.xml new file mode 100644 index 000000000000..014473c4ea80 --- /dev/null +++ b/app/src/gplay/res/drawable-night/file_movie.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/gplay/res/drawable-night/file_sound.xml b/app/src/gplay/res/drawable-night/file_sound.xml new file mode 100644 index 000000000000..0739329de55b --- /dev/null +++ b/app/src/gplay/res/drawable-night/file_sound.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable-night/shared_via_link.xml b/app/src/gplay/res/drawable-night/shared_via_link.xml new file mode 100644 index 000000000000..f4ef3c4ff1cc --- /dev/null +++ b/app/src/gplay/res/drawable-night/shared_via_link.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/gplay/res/drawable-night/shared_via_users.xml b/app/src/gplay/res/drawable-night/shared_via_users.xml new file mode 100644 index 000000000000..e0389e0ee4b3 --- /dev/null +++ b/app/src/gplay/res/drawable-night/shared_via_users.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/account_circle_white.xml b/app/src/gplay/res/drawable/account_circle_white.xml new file mode 100644 index 000000000000..e8ea2101d106 --- /dev/null +++ b/app/src/gplay/res/drawable/account_circle_white.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/add_to_home_screen.xml b/app/src/gplay/res/drawable/add_to_home_screen.xml new file mode 100644 index 000000000000..12bad2c11102 --- /dev/null +++ b/app/src/gplay/res/drawable/add_to_home_screen.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/all_files.xml b/app/src/gplay/res/drawable/all_files.xml new file mode 100644 index 000000000000..9efb2dcd0327 --- /dev/null +++ b/app/src/gplay/res/drawable/all_files.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/bottom_sheet_drag_handle.xml b/app/src/gplay/res/drawable/bottom_sheet_drag_handle.xml new file mode 100644 index 000000000000..e060526252b1 --- /dev/null +++ b/app/src/gplay/res/drawable/bottom_sheet_drag_handle.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/gplay/res/drawable/dialog_background.xml b/app/src/gplay/res/drawable/dialog_background.xml new file mode 100644 index 000000000000..6f14d4c3f227 --- /dev/null +++ b/app/src/gplay/res/drawable/dialog_background.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/dialog_item_background.xml b/app/src/gplay/res/drawable/dialog_item_background.xml new file mode 100644 index 000000000000..cbcdd3ff5263 --- /dev/null +++ b/app/src/gplay/res/drawable/dialog_item_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/e2e_border.xml b/app/src/gplay/res/drawable/e2e_border.xml new file mode 100644 index 000000000000..a545c168e793 --- /dev/null +++ b/app/src/gplay/res/drawable/e2e_border.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/exo_divider.xml b/app/src/gplay/res/drawable/exo_divider.xml new file mode 100644 index 000000000000..205032f4f73d --- /dev/null +++ b/app/src/gplay/res/drawable/exo_divider.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_ic_check.xml b/app/src/gplay/res/drawable/exo_ic_check.xml new file mode 100644 index 000000000000..d045c84f5db5 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_ic_check.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_audiotrack.xml b/app/src/gplay/res/drawable/exo_styled_controls_audiotrack.xml new file mode 100644 index 000000000000..fd50f84d0482 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_audiotrack.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_pause.xml b/app/src/gplay/res/drawable/exo_styled_controls_pause.xml new file mode 100644 index 000000000000..c45c39df05da --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_pause.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_play.xml b/app/src/gplay/res/drawable/exo_styled_controls_play.xml new file mode 100644 index 000000000000..1956ab230365 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_play.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_settings.xml b/app/src/gplay/res/drawable/exo_styled_controls_settings.xml new file mode 100644 index 000000000000..d0a127203c85 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_settings.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_simple_fastforward.xml b/app/src/gplay/res/drawable/exo_styled_controls_simple_fastforward.xml new file mode 100644 index 000000000000..11002787d609 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_simple_fastforward.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_simple_rewind.xml b/app/src/gplay/res/drawable/exo_styled_controls_simple_rewind.xml new file mode 100644 index 000000000000..c7d2782ea707 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_simple_rewind.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_styled_controls_speed.xml b/app/src/gplay/res/drawable/exo_styled_controls_speed.xml new file mode 100644 index 000000000000..e10f5e2223e9 --- /dev/null +++ b/app/src/gplay/res/drawable/exo_styled_controls_speed.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/exo_thumb.xml b/app/src/gplay/res/drawable/exo_thumb.xml new file mode 100644 index 000000000000..d31859726d0a --- /dev/null +++ b/app/src/gplay/res/drawable/exo_thumb.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/favorite.xml b/app/src/gplay/res/drawable/favorite.xml new file mode 100644 index 000000000000..44a725f11469 --- /dev/null +++ b/app/src/gplay/res/drawable/favorite.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/gplay/res/drawable/file.xml b/app/src/gplay/res/drawable/file.xml new file mode 100644 index 000000000000..bb46adbe85de --- /dev/null +++ b/app/src/gplay/res/drawable/file.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/file_calendar.xml b/app/src/gplay/res/drawable/file_calendar.xml new file mode 100644 index 000000000000..d8b720a581e1 --- /dev/null +++ b/app/src/gplay/res/drawable/file_calendar.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/file_details_search_background.xml b/app/src/gplay/res/drawable/file_details_search_background.xml new file mode 100644 index 000000000000..568a130edcd4 --- /dev/null +++ b/app/src/gplay/res/drawable/file_details_search_background.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/file_image.xml b/app/src/gplay/res/drawable/file_image.xml new file mode 100644 index 000000000000..5b205f0cad09 --- /dev/null +++ b/app/src/gplay/res/drawable/file_image.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/file_movie.xml b/app/src/gplay/res/drawable/file_movie.xml new file mode 100644 index 000000000000..c4540c26eb51 --- /dev/null +++ b/app/src/gplay/res/drawable/file_movie.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/file_multiple.xml b/app/src/gplay/res/drawable/file_multiple.xml new file mode 100644 index 000000000000..942c9373d4bc --- /dev/null +++ b/app/src/gplay/res/drawable/file_multiple.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/file_sound.xml b/app/src/gplay/res/drawable/file_sound.xml new file mode 100644 index 000000000000..63caf9489b6b --- /dev/null +++ b/app/src/gplay/res/drawable/file_sound.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/filelist_action_icon_background.xml b/app/src/gplay/res/drawable/filelist_action_icon_background.xml new file mode 100644 index 000000000000..ad4d33647d47 --- /dev/null +++ b/app/src/gplay/res/drawable/filelist_action_icon_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/folder.xml b/app/src/gplay/res/drawable/folder.xml new file mode 100644 index 000000000000..8b4fc8a42890 --- /dev/null +++ b/app/src/gplay/res/drawable/folder.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/folder_star_32dp.xml b/app/src/gplay/res/drawable/folder_star_32dp.xml new file mode 100644 index 000000000000..8b4fc8a42890 --- /dev/null +++ b/app/src/gplay/res/drawable/folder_star_32dp.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/grid_mode_item_background.xml b/app/src/gplay/res/drawable/grid_mode_item_background.xml new file mode 100644 index 000000000000..c3bb6301c093 --- /dev/null +++ b/app/src/gplay/res/drawable/grid_mode_item_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/grid_mode_item_outline.xml b/app/src/gplay/res/drawable/grid_mode_item_outline.xml new file mode 100644 index 000000000000..670be17089d1 --- /dev/null +++ b/app/src/gplay/res/drawable/grid_mode_item_outline.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/grid_mode_more_button_background.xml b/app/src/gplay/res/drawable/grid_mode_more_button_background.xml new file mode 100644 index 000000000000..3dd44347922f --- /dev/null +++ b/app/src/gplay/res/drawable/grid_mode_more_button_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/grid_mode_selected_item_background.xml b/app/src/gplay/res/drawable/grid_mode_selected_item_background.xml new file mode 100644 index 000000000000..756aa8df241b --- /dev/null +++ b/app/src/gplay/res/drawable/grid_mode_selected_item_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_action_create_dir.xml b/app/src/gplay/res/drawable/ic_action_create_dir.xml new file mode 100644 index 000000000000..2141f7c78e49 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_action_create_dir.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_action_upload.xml b/app/src/gplay/res/drawable/ic_action_upload.xml new file mode 100644 index 000000000000..c8a9e0631b0b --- /dev/null +++ b/app/src/gplay/res/drawable/ic_action_upload.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_activity.xml b/app/src/gplay/res/drawable/ic_activity.xml new file mode 100644 index 000000000000..2cf83e115e53 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_activity.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_alphabetical_asc.xml b/app/src/gplay/res/drawable/ic_alphabetical_asc.xml new file mode 100644 index 000000000000..39545cafc5fc --- /dev/null +++ b/app/src/gplay/res/drawable/ic_alphabetical_asc.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_alphabetical_desc.xml b/app/src/gplay/res/drawable/ic_alphabetical_desc.xml new file mode 100644 index 000000000000..f6c98c61d5f6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_alphabetical_desc.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_arrow_back.xml b/app/src/gplay/res/drawable/ic_arrow_back.xml new file mode 100644 index 000000000000..114ef8a5760e --- /dev/null +++ b/app/src/gplay/res/drawable/ic_arrow_back.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_assistant.xml b/app/src/gplay/res/drawable/ic_assistant.xml new file mode 100644 index 000000000000..314febca6a85 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_assistant.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_camera.xml b/app/src/gplay/res/drawable/ic_camera.xml new file mode 100644 index 000000000000..eaaaecd77eb6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_camera.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_checkbox_blank_outline.xml b/app/src/gplay/res/drawable/ic_checkbox_blank_outline.xml new file mode 100644 index 000000000000..acbdc3c8bb72 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_checkbox_blank_outline.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_checkbox_marked.xml b/app/src/gplay/res/drawable/ic_checkbox_marked.xml new file mode 100644 index 000000000000..d249debaeaab --- /dev/null +++ b/app/src/gplay/res/drawable/ic_checkbox_marked.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_close_search.xml b/app/src/gplay/res/drawable/ic_close_search.xml new file mode 100644 index 000000000000..f5be8d6d699d --- /dev/null +++ b/app/src/gplay/res/drawable/ic_close_search.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud.xml b/app/src/gplay/res/drawable/ic_cloud.xml new file mode 100644 index 000000000000..1fadad4368de --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud_download.xml b/app/src/gplay/res/drawable/ic_cloud_download.xml new file mode 100644 index 000000000000..37592e253cd6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud_download.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud_sync.xml b/app/src/gplay/res/drawable/ic_cloud_sync.xml new file mode 100644 index 000000000000..5c11e1c68aa0 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud_sync.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud_sync_off.xml b/app/src/gplay/res/drawable/ic_cloud_sync_off.xml new file mode 100644 index 000000000000..37f4d285ba23 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud_sync_off.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud_sync_on.xml b/app/src/gplay/res/drawable/ic_cloud_sync_on.xml new file mode 100644 index 000000000000..3de3b0a40947 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud_sync_on.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_cloud_upload.xml b/app/src/gplay/res/drawable/ic_cloud_upload.xml new file mode 100644 index 000000000000..c4bc7f3dd6ba --- /dev/null +++ b/app/src/gplay/res/drawable/ic_cloud_upload.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_contact_book.xml b/app/src/gplay/res/drawable/ic_contact_book.xml new file mode 100644 index 000000000000..81673b5c8484 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_contact_book.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_content_copy.xml b/app/src/gplay/res/drawable/ic_content_copy.xml new file mode 100644 index 000000000000..3cfc1e42d971 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_content_copy.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_delete.xml b/app/src/gplay/res/drawable/ic_delete.xml new file mode 100644 index 000000000000..8a0b2f618a58 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_delete.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_dots_vertical.xml b/app/src/gplay/res/drawable/ic_dots_vertical.xml new file mode 100644 index 000000000000..e572128d3612 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_dots_vertical.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_edit.xml b/app/src/gplay/res/drawable/ic_edit.xml new file mode 100644 index 000000000000..b70c7eae487c --- /dev/null +++ b/app/src/gplay/res/drawable/ic_edit.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_email.xml b/app/src/gplay/res/drawable/ic_email.xml new file mode 100644 index 000000000000..5c1a5a36f8da --- /dev/null +++ b/app/src/gplay/res/drawable/ic_email.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_export.xml b/app/src/gplay/res/drawable/ic_export.xml new file mode 100644 index 000000000000..d331d6fb1b57 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_export.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_cancel_sync.xml b/app/src/gplay/res/drawable/ic_file_action_cancel_sync.xml new file mode 100644 index 000000000000..0a9173c21d08 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_cancel_sync.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_edit.xml b/app/src/gplay/res/drawable/ic_file_action_edit.xml new file mode 100644 index 000000000000..43233ac3db1d --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_edit.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_lock_file.xml b/app/src/gplay/res/drawable/ic_file_action_lock_file.xml new file mode 100644 index 000000000000..f83bf38fd62d --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_lock_file.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_move_or_copy.xml b/app/src/gplay/res/drawable/ic_file_action_move_or_copy.xml new file mode 100644 index 000000000000..1d9b33e52e55 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_move_or_copy.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_open_file_with.xml b/app/src/gplay/res/drawable/ic_file_action_open_file_with.xml new file mode 100644 index 000000000000..ebd25250a8ec --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_open_file_with.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_set_encrypted.xml b/app/src/gplay/res/drawable/ic_file_action_set_encrypted.xml new file mode 100644 index 000000000000..ca99659b4629 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_set_encrypted.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_stream_media.xml b/app/src/gplay/res/drawable/ic_file_action_stream_media.xml new file mode 100644 index 000000000000..9e4d0662e523 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_stream_media.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_sync_file.xml b/app/src/gplay/res/drawable/ic_file_action_sync_file.xml new file mode 100644 index 000000000000..297aa442036f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_sync_file.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_unlock_file.xml b/app/src/gplay/res/drawable/ic_file_action_unlock_file.xml new file mode 100644 index 000000000000..d4724ca67180 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_unlock_file.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_file_action_unset_encrypted.xml b/app/src/gplay/res/drawable/ic_file_action_unset_encrypted.xml new file mode 100644 index 000000000000..20a8f656fd71 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_file_action_unset_encrypted.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_account_group.xml b/app/src/gplay/res/drawable/ic_folder_overlay_account_group.xml new file mode 100644 index 000000000000..05e232d6895f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_account_group.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_external.xml b/app/src/gplay/res/drawable/ic_folder_overlay_external.xml new file mode 100644 index 000000000000..7eb67c1c67cb --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_external.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_key.xml b/app/src/gplay/res/drawable/ic_folder_overlay_key.xml new file mode 100644 index 000000000000..181769358a26 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_key.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_link.xml b/app/src/gplay/res/drawable/ic_folder_overlay_link.xml new file mode 100644 index 000000000000..d87d6603ae6b --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_link.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_lock.xml b/app/src/gplay/res/drawable/ic_folder_overlay_lock.xml new file mode 100644 index 000000000000..db30297451b6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_lock.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_share.xml b/app/src/gplay/res/drawable/ic_folder_overlay_share.xml new file mode 100644 index 000000000000..05e232d6895f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_share.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_folder_overlay_upload.xml b/app/src/gplay/res/drawable/ic_folder_overlay_upload.xml new file mode 100644 index 000000000000..afe3192f83aa --- /dev/null +++ b/app/src/gplay/res/drawable/ic_folder_overlay_upload.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_gallery_set_media_folder.xml b/app/src/gplay/res/drawable/ic_gallery_set_media_folder.xml new file mode 100644 index 000000000000..fdaa13d8c77d --- /dev/null +++ b/app/src/gplay/res/drawable/ic_gallery_set_media_folder.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_gallery_show_photos.xml b/app/src/gplay/res/drawable/ic_gallery_show_photos.xml new file mode 100644 index 000000000000..eaaaecd77eb6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_gallery_show_photos.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_gallery_show_videos.xml b/app/src/gplay/res/drawable/ic_gallery_show_videos.xml new file mode 100644 index 000000000000..a8ea6b923ae0 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_gallery_show_videos.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_gallery_tick.xml b/app/src/gplay/res/drawable/ic_gallery_tick.xml new file mode 100644 index 000000000000..05a9c15ca270 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_gallery_tick.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_grid_mode_more.xml b/app/src/gplay/res/drawable/ic_grid_mode_more.xml new file mode 100644 index 000000000000..9d9291500985 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_grid_mode_more.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_history.xml b/app/src/gplay/res/drawable/ic_history.xml new file mode 100644 index 000000000000..82b62c934363 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_history.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_import.xml b/app/src/gplay/res/drawable/ic_import.xml new file mode 100644 index 000000000000..b7523313a2c1 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_import.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_information_outline.xml b/app/src/gplay/res/drawable/ic_information_outline.xml new file mode 100644 index 000000000000..62f45aa6f038 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_information_outline.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_link.xml b/app/src/gplay/res/drawable/ic_link.xml new file mode 100644 index 000000000000..b1666422bcc6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_link.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_list_empty_create_folder.xml b/app/src/gplay/res/drawable/ic_list_empty_create_folder.xml new file mode 100644 index 000000000000..2af06159b73c --- /dev/null +++ b/app/src/gplay/res/drawable/ic_list_empty_create_folder.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_list_empty_error.xml b/app/src/gplay/res/drawable/ic_list_empty_error.xml new file mode 100644 index 000000000000..4b64a4e49553 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_list_empty_error.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_list_empty_folder.xml b/app/src/gplay/res/drawable/ic_list_empty_folder.xml new file mode 100644 index 000000000000..bbeb3f1d05ee --- /dev/null +++ b/app/src/gplay/res/drawable/ic_list_empty_folder.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_list_empty_recent.xml b/app/src/gplay/res/drawable/ic_list_empty_recent.xml new file mode 100644 index 000000000000..676879e62d6e --- /dev/null +++ b/app/src/gplay/res/drawable/ic_list_empty_recent.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_list_empty_shared.xml b/app/src/gplay/res/drawable/ic_list_empty_shared.xml new file mode 100644 index 000000000000..5cf82c43f9f6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_list_empty_shared.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_menu.xml b/app/src/gplay/res/drawable/ic_menu.xml new file mode 100644 index 000000000000..d3ae8c8b3080 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_menu.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_modification_asc.xml b/app/src/gplay/res/drawable/ic_modification_asc.xml new file mode 100644 index 000000000000..19f367facb48 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_modification_asc.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_modification_desc.xml b/app/src/gplay/res/drawable/ic_modification_desc.xml new file mode 100644 index 000000000000..58c619363535 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_modification_desc.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_pause.xml b/app/src/gplay/res/drawable/ic_pause.xml new file mode 100644 index 000000000000..af11ea628327 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_pause.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/drawable/ic_play.xml b/app/src/gplay/res/drawable/ic_play.xml new file mode 100644 index 000000000000..d151770705cf --- /dev/null +++ b/app/src/gplay/res/drawable/ic_play.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/drawable/ic_plus.xml b/app/src/gplay/res/drawable/ic_plus.xml new file mode 100644 index 000000000000..1990e792ad8f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_plus.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_progress_bar_login_indeterminate.xml b/app/src/gplay/res/drawable/ic_progress_bar_login_indeterminate.xml new file mode 100644 index 000000000000..0a5a500b19f7 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_progress_bar_login_indeterminate.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/gplay/res/drawable/ic_rename.xml b/app/src/gplay/res/drawable/ic_rename.xml new file mode 100644 index 000000000000..e2a4df854a82 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_rename.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_scan_document.xml b/app/src/gplay/res/drawable/ic_scan_document.xml new file mode 100644 index 000000000000..8799c9510fbb --- /dev/null +++ b/app/src/gplay/res/drawable/ic_scan_document.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_search.xml b/app/src/gplay/res/drawable/ic_search.xml new file mode 100644 index 000000000000..f6d35d2f2bcc --- /dev/null +++ b/app/src/gplay/res/drawable/ic_search.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_search_grey.xml b/app/src/gplay/res/drawable/ic_search_grey.xml new file mode 100644 index 000000000000..87e41fad695a --- /dev/null +++ b/app/src/gplay/res/drawable/ic_search_grey.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_select_all.xml b/app/src/gplay/res/drawable/ic_select_all.xml new file mode 100644 index 000000000000..50db787d70ae --- /dev/null +++ b/app/src/gplay/res/drawable/ic_select_all.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_select_none.xml b/app/src/gplay/res/drawable/ic_select_none.xml new file mode 100644 index 000000000000..929269719e69 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_select_none.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_share.xml b/app/src/gplay/res/drawable/ic_share.xml new file mode 100644 index 000000000000..5cf82c43f9f6 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_share.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_share_settings.xml b/app/src/gplay/res/drawable/ic_share_settings.xml new file mode 100644 index 000000000000..91a6d5e62bbd --- /dev/null +++ b/app/src/gplay/res/drawable/ic_share_settings.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_size_asc.xml b/app/src/gplay/res/drawable/ic_size_asc.xml new file mode 100644 index 000000000000..08e0b390792f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_size_asc.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_size_desc.xml b/app/src/gplay/res/drawable/ic_size_desc.xml new file mode 100644 index 000000000000..d0ffd2c9d99e --- /dev/null +++ b/app/src/gplay/res/drawable/ic_size_desc.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_star.xml b/app/src/gplay/res/drawable/ic_star.xml new file mode 100644 index 000000000000..f03c59e85bf1 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_star.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_star_outline.xml b/app/src/gplay/res/drawable/ic_star_outline.xml new file mode 100644 index 000000000000..f38d5c4ea3da --- /dev/null +++ b/app/src/gplay/res/drawable/ic_star_outline.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_synced.xml b/app/src/gplay/res/drawable/ic_synced.xml new file mode 100644 index 000000000000..31b565901f3b --- /dev/null +++ b/app/src/gplay/res/drawable/ic_synced.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/ic_tick.xml b/app/src/gplay/res/drawable/ic_tick.xml new file mode 100644 index 000000000000..f4716e25109c --- /dev/null +++ b/app/src/gplay/res/drawable/ic_tick.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_unshare.xml b/app/src/gplay/res/drawable/ic_unshare.xml new file mode 100644 index 000000000000..c6c5b6bba69c --- /dev/null +++ b/app/src/gplay/res/drawable/ic_unshare.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_unshared.xml b/app/src/gplay/res/drawable/ic_unshared.xml new file mode 100644 index 000000000000..e11da31a265e --- /dev/null +++ b/app/src/gplay/res/drawable/ic_unshared.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_user.xml b/app/src/gplay/res/drawable/ic_user.xml new file mode 100644 index 000000000000..6517166eeba1 --- /dev/null +++ b/app/src/gplay/res/drawable/ic_user.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_view_list.xml b/app/src/gplay/res/drawable/ic_view_list.xml new file mode 100644 index 000000000000..3a287644bd8f --- /dev/null +++ b/app/src/gplay/res/drawable/ic_view_list.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_view_module.xml b/app/src/gplay/res/drawable/ic_view_module.xml new file mode 100644 index 000000000000..8e96d7dfb9bb --- /dev/null +++ b/app/src/gplay/res/drawable/ic_view_module.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/ic_wallpaper.xml b/app/src/gplay/res/drawable/ic_wallpaper.xml new file mode 100644 index 000000000000..fdaa13d8c77d --- /dev/null +++ b/app/src/gplay/res/drawable/ic_wallpaper.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/image_32dp.xml b/app/src/gplay/res/drawable/image_32dp.xml new file mode 100644 index 000000000000..6f4dfe06df57 --- /dev/null +++ b/app/src/gplay/res/drawable/image_32dp.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_favorites.xml b/app/src/gplay/res/drawable/nav_favorites.xml new file mode 100644 index 000000000000..f03c59e85bf1 --- /dev/null +++ b/app/src/gplay/res/drawable/nav_favorites.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_notifications.xml b/app/src/gplay/res/drawable/nav_notifications.xml new file mode 100644 index 000000000000..f8c12d5af7dc --- /dev/null +++ b/app/src/gplay/res/drawable/nav_notifications.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/gplay/res/drawable/nav_on_device.xml b/app/src/gplay/res/drawable/nav_on_device.xml new file mode 100644 index 000000000000..902842d03802 --- /dev/null +++ b/app/src/gplay/res/drawable/nav_on_device.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_photos.xml b/app/src/gplay/res/drawable/nav_photos.xml new file mode 100644 index 000000000000..fdaa13d8c77d --- /dev/null +++ b/app/src/gplay/res/drawable/nav_photos.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_settings.xml b/app/src/gplay/res/drawable/nav_settings.xml new file mode 100644 index 000000000000..dfa36583d70f --- /dev/null +++ b/app/src/gplay/res/drawable/nav_settings.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_shared.xml b/app/src/gplay/res/drawable/nav_shared.xml new file mode 100644 index 000000000000..d136bc23398e --- /dev/null +++ b/app/src/gplay/res/drawable/nav_shared.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/nav_trashbin.xml b/app/src/gplay/res/drawable/nav_trashbin.xml new file mode 100644 index 000000000000..75e40f031021 --- /dev/null +++ b/app/src/gplay/res/drawable/nav_trashbin.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/notification_icon.xml b/app/src/gplay/res/drawable/notification_icon.xml new file mode 100644 index 000000000000..af51be5061f0 --- /dev/null +++ b/app/src/gplay/res/drawable/notification_icon.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/app/src/gplay/res/drawable/notifications_icon.xml b/app/src/gplay/res/drawable/notifications_icon.xml new file mode 100644 index 000000000000..7fbc42b42e90 --- /dev/null +++ b/app/src/gplay/res/drawable/notifications_icon.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_audio.xml b/app/src/gplay/res/drawable/player_ic_audio.xml new file mode 100644 index 000000000000..de22932cc45e --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_audio.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_pause.xml b/app/src/gplay/res/drawable/player_ic_pause.xml new file mode 100644 index 000000000000..ee9e42e03909 --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_pause.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_play.xml b/app/src/gplay/res/drawable/player_ic_play.xml new file mode 100644 index 000000000000..44221a16e22c --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_play.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_repeat.xml b/app/src/gplay/res/drawable/player_ic_repeat.xml new file mode 100644 index 000000000000..7d5f72f7e7d8 --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_repeat.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_shuffle.xml b/app/src/gplay/res/drawable/player_ic_shuffle.xml new file mode 100644 index 000000000000..5740454bf6c5 --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_shuffle.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_skip_next.xml b/app/src/gplay/res/drawable/player_ic_skip_next.xml new file mode 100644 index 000000000000..22b018dca37f --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_skip_next.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_skip_previous.xml b/app/src/gplay/res/drawable/player_ic_skip_previous.xml new file mode 100644 index 000000000000..36ceb9269132 --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_skip_previous.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/player_ic_video.xml b/app/src/gplay/res/drawable/player_ic_video.xml new file mode 100644 index 000000000000..02a47db367bb --- /dev/null +++ b/app/src/gplay/res/drawable/player_ic_video.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/gplay/res/drawable/progress_bar_login_indeterminate.xml b/app/src/gplay/res/drawable/progress_bar_login_indeterminate.xml new file mode 100644 index 000000000000..01762da59db2 --- /dev/null +++ b/app/src/gplay/res/drawable/progress_bar_login_indeterminate.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/shared_via_link.xml b/app/src/gplay/res/drawable/shared_via_link.xml new file mode 100644 index 000000000000..f4ef3c4ff1cc --- /dev/null +++ b/app/src/gplay/res/drawable/shared_via_link.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/gplay/res/drawable/shared_via_users.xml b/app/src/gplay/res/drawable/shared_via_users.xml new file mode 100644 index 000000000000..e0389e0ee4b3 --- /dev/null +++ b/app/src/gplay/res/drawable/shared_via_users.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/app/src/gplay/res/drawable/uploads.xml b/app/src/gplay/res/drawable/uploads.xml new file mode 100644 index 000000000000..c26710c81044 --- /dev/null +++ b/app/src/gplay/res/drawable/uploads.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/video_32dp.xml b/app/src/gplay/res/drawable/video_32dp.xml new file mode 100644 index 000000000000..d3e53fe78cd5 --- /dev/null +++ b/app/src/gplay/res/drawable/video_32dp.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/drawable/video_white.xml b/app/src/gplay/res/drawable/video_white.xml new file mode 100644 index 000000000000..a7cfa137b3a0 --- /dev/null +++ b/app/src/gplay/res/drawable/video_white.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/gplay/res/layout/account_setup.xml b/app/src/gplay/res/layout/account_setup.xml new file mode 100644 index 000000000000..ce4eb0093452 --- /dev/null +++ b/app/src/gplay/res/layout/account_setup.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/account_setup_webview.xml b/app/src/gplay/res/layout/account_setup_webview.xml new file mode 100644 index 000000000000..6653add935e3 --- /dev/null +++ b/app/src/gplay/res/layout/account_setup_webview.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/activity_preview_media.xml b/app/src/gplay/res/layout/activity_preview_media.xml new file mode 100644 index 000000000000..0d995dd2c0f0 --- /dev/null +++ b/app/src/gplay/res/layout/activity_preview_media.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/activity_splash.xml b/app/src/gplay/res/layout/activity_splash.xml new file mode 100644 index 000000000000..a26af2ef1c45 --- /dev/null +++ b/app/src/gplay/res/layout/activity_splash.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/backup_fragment.xml b/app/src/gplay/res/layout/backup_fragment.xml new file mode 100644 index 000000000000..40db4e6c3385 --- /dev/null +++ b/app/src/gplay/res/layout/backup_fragment.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/deep_link_login.xml b/app/src/gplay/res/layout/deep_link_login.xml new file mode 100644 index 000000000000..1dbf1c43d6fa --- /dev/null +++ b/app/src/gplay/res/layout/deep_link_login.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/drawer.xml b/app/src/gplay/res/layout/drawer.xml new file mode 100644 index 000000000000..9286d26aece6 --- /dev/null +++ b/app/src/gplay/res/layout/drawer.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/empty_list_media.xml b/app/src/gplay/res/layout/empty_list_media.xml new file mode 100644 index 000000000000..9c6e8c95fc45 --- /dev/null +++ b/app/src/gplay/res/layout/empty_list_media.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/exo_player_control_view.xml b/app/src/gplay/res/layout/exo_player_control_view.xml new file mode 100644 index 000000000000..3f457f3a5563 --- /dev/null +++ b/app/src/gplay/res/layout/exo_player_control_view.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/exo_player_view.xml b/app/src/gplay/res/layout/exo_player_view.xml new file mode 100644 index 000000000000..930b014b1d30 --- /dev/null +++ b/app/src/gplay/res/layout/exo_player_view.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_actions_bottom_sheet.xml b/app/src/gplay/res/layout/file_actions_bottom_sheet.xml new file mode 100644 index 000000000000..ec2c6d60caf3 --- /dev/null +++ b/app/src/gplay/res/layout/file_actions_bottom_sheet.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_actions_bottom_sheet_item.xml b/app/src/gplay/res/layout/file_actions_bottom_sheet_item.xml new file mode 100644 index 000000000000..1fac004a33a2 --- /dev/null +++ b/app/src/gplay/res/layout/file_actions_bottom_sheet_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_details_fragment.xml b/app/src/gplay/res/layout/file_details_fragment.xml new file mode 100644 index 000000000000..ed791047783e --- /dev/null +++ b/app/src/gplay/res/layout/file_details_fragment.xml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_details_share_link_share_item.xml b/app/src/gplay/res/layout/file_details_share_link_share_item.xml new file mode 100644 index 000000000000..d307f152bc15 --- /dev/null +++ b/app/src/gplay/res/layout/file_details_share_link_share_item.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_details_sharing_fragment.xml b/app/src/gplay/res/layout/file_details_sharing_fragment.xml new file mode 100644 index 000000000000..d1995b3409b0 --- /dev/null +++ b/app/src/gplay/res/layout/file_details_sharing_fragment.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml b/app/src/gplay/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml new file mode 100644 index 000000000000..b49d76853225 --- /dev/null +++ b/app/src/gplay/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/fragment_gallery_bottom_sheet.xml b/app/src/gplay/res/layout/fragment_gallery_bottom_sheet.xml new file mode 100644 index 000000000000..139a9a827c6b --- /dev/null +++ b/app/src/gplay/res/layout/fragment_gallery_bottom_sheet.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/grid_item.xml b/app/src/gplay/res/layout/grid_item.xml new file mode 100644 index 000000000000..93a7c40526fe --- /dev/null +++ b/app/src/gplay/res/layout/grid_item.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/info_box.xml b/app/src/gplay/res/layout/info_box.xml new file mode 100644 index 000000000000..40d99656dd68 --- /dev/null +++ b/app/src/gplay/res/layout/info_box.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/gplay/res/layout/item_quick_share_permissions.xml b/app/src/gplay/res/layout/item_quick_share_permissions.xml new file mode 100644 index 000000000000..7f300106faae --- /dev/null +++ b/app/src/gplay/res/layout/item_quick_share_permissions.xml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/list_item.xml b/app/src/gplay/res/layout/list_item.xml new file mode 100644 index 000000000000..ecb4e637b95f --- /dev/null +++ b/app/src/gplay/res/layout/list_item.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/login_flow_info_layout_v2.xml b/app/src/gplay/res/layout/login_flow_info_layout_v2.xml new file mode 100644 index 000000000000..3227876ee40e --- /dev/null +++ b/app/src/gplay/res/layout/login_flow_info_layout_v2.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/media_control.xml b/app/src/gplay/res/layout/media_control.xml new file mode 100644 index 000000000000..2b13b15d87b3 --- /dev/null +++ b/app/src/gplay/res/layout/media_control.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml b/app/src/gplay/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml new file mode 100644 index 000000000000..ae1fd24ba225 --- /dev/null +++ b/app/src/gplay/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/gplay/res/layout/settings_preference_first_group.xml b/app/src/gplay/res/layout/settings_preference_first_group.xml new file mode 100644 index 000000000000..f207114a078e --- /dev/null +++ b/app/src/gplay/res/layout/settings_preference_first_group.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/settings_preference_group.xml b/app/src/gplay/res/layout/settings_preference_group.xml new file mode 100644 index 000000000000..56d0dd055f0e --- /dev/null +++ b/app/src/gplay/res/layout/settings_preference_group.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/settings_preference_list_item.xml b/app/src/gplay/res/layout/settings_preference_list_item.xml new file mode 100644 index 000000000000..5fddbb9bf057 --- /dev/null +++ b/app/src/gplay/res/layout/settings_preference_list_item.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/gplay/res/layout/settings_preference_switcher.xml b/app/src/gplay/res/layout/settings_preference_switcher.xml new file mode 100644 index 000000000000..a2b18f7b8ef3 --- /dev/null +++ b/app/src/gplay/res/layout/settings_preference_switcher.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/settings_privacy_switchers.xml b/app/src/gplay/res/layout/settings_privacy_switchers.xml new file mode 100644 index 000000000000..e10c46b377ab --- /dev/null +++ b/app/src/gplay/res/layout/settings_privacy_switchers.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/share_activity.xml b/app/src/gplay/res/layout/share_activity.xml new file mode 100644 index 000000000000..54f9244dfc82 --- /dev/null +++ b/app/src/gplay/res/layout/share_activity.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml b/app/src/gplay/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml new file mode 100644 index 000000000000..197853d4bc9e --- /dev/null +++ b/app/src/gplay/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/simple_spinner_item.xml b/app/src/gplay/res/layout/simple_spinner_item.xml new file mode 100644 index 000000000000..4b6e91765dd6 --- /dev/null +++ b/app/src/gplay/res/layout/simple_spinner_item.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/gplay/res/layout/sorting_order_fragment.xml b/app/src/gplay/res/layout/sorting_order_fragment.xml new file mode 100644 index 000000000000..ad8ff4cd8e72 --- /dev/null +++ b/app/src/gplay/res/layout/sorting_order_fragment.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/synced_folders_item_header.xml b/app/src/gplay/res/layout/synced_folders_item_header.xml new file mode 100644 index 000000000000..71ba1f746886 --- /dev/null +++ b/app/src/gplay/res/layout/synced_folders_item_header.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/toolbar_standard.xml b/app/src/gplay/res/layout/toolbar_standard.xml new file mode 100644 index 000000000000..f45520df420d --- /dev/null +++ b/app/src/gplay/res/layout/toolbar_standard.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/layout/view_authorization_method.xml b/app/src/gplay/res/layout/view_authorization_method.xml new file mode 100644 index 000000000000..85d8ec412ce3 --- /dev/null +++ b/app/src/gplay/res/layout/view_authorization_method.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/layout/view_login_background.xml b/app/src/gplay/res/layout/view_login_background.xml new file mode 100644 index 000000000000..80064d2ab3c1 --- /dev/null +++ b/app/src/gplay/res/layout/view_login_background.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/gplay/res/menu/partial_drawer_entries.xml b/app/src/gplay/res/menu/partial_drawer_entries.xml new file mode 100644 index 000000000000..1c2fee08ec11 --- /dev/null +++ b/app/src/gplay/res/menu/partial_drawer_entries.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/values-land/ionos-dimens.xml b/app/src/gplay/res/values-land/ionos-dimens.xml new file mode 100644 index 000000000000..62bf3d0ab28b --- /dev/null +++ b/app/src/gplay/res/values-land/ionos-dimens.xml @@ -0,0 +1,15 @@ + + + + + + 0.05 + 0.4 + 0.65 + + \ No newline at end of file diff --git a/app/src/gplay/res/values-large/ionos-dimens.xml b/app/src/gplay/res/values-large/ionos-dimens.xml new file mode 100644 index 000000000000..a008276d3d92 --- /dev/null +++ b/app/src/gplay/res/values-large/ionos-dimens.xml @@ -0,0 +1,30 @@ + + + + + + + 36dp + 48dp + 36dp + 12dp + 48dp + 12dp + 12dp + 48dp + 8dp + 16sp + 14sp + + 16dp + 56dp + 72dp + 8dp + 80dp + + \ No newline at end of file diff --git a/app/src/gplay/res/values-v27/ionos-styles.xml b/app/src/gplay/res/values-v27/ionos-styles.xml new file mode 100644 index 000000000000..5cfc7277d38d --- /dev/null +++ b/app/src/gplay/res/values-v27/ionos-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/gplay/res/values/ionos-dimens.xml b/app/src/gplay/res/values/ionos-dimens.xml new file mode 100644 index 000000000000..af9280e137e1 --- /dev/null +++ b/app/src/gplay/res/values/ionos-dimens.xml @@ -0,0 +1,85 @@ + + + + + + + 0.2 + 0.4 + 0.8 + + + 8dp + 16dp + 24dp + 8dp + 16dp + 8dp + 8dp + 16dp + 8dp + 16sp + 14sp + + + 40dp + 44dp + 52dp + 32dp + 96dp + 22dp + 6dp + 24dp + 24dp + + 44dp + 64dp + + 8dp + 56dp + 32dp + 4dp + 80dp + + 16sp + 12sp + 24sp + + 24dp + 24dp + 24dp + 48dp + 12dp + 12dp + 16dp + 16dp + 12dp + + 24dp + 12dp + 12dp + 8dp + 12dp + 12dp + + 12dp + + 32dp + 4dp + 5dp + + 1dp + 8dp + 14sp + 16dp + + 1000000dp + + 1 + + \ No newline at end of file diff --git a/app/src/gplay/res/values/ionos-styles.xml b/app/src/gplay/res/values/ionos-styles.xml new file mode 100644 index 000000000000..460093ad5014 --- /dev/null +++ b/app/src/gplay/res/values/ionos-styles.xml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/res/values/player-colors.xml b/app/src/gplay/res/values/player-colors.xml new file mode 100644 index 000000000000..144499edd9f3 --- /dev/null +++ b/app/src/gplay/res/values/player-colors.xml @@ -0,0 +1,16 @@ + + + + @color/dodger_blue + @color/midnight_blue2 + @color/lavender_mist + @color/lavender_mist + @color/transparent_midnight_blue2 + @color/transparent_midnight_blue2 + @color/gull_grayapprox + \ No newline at end of file diff --git a/app/src/gplay/res/values/setup.xml b/app/src/gplay/res/values/setup.xml deleted file mode 100644 index 83fadc4e35d1..000000000000 --- a/app/src/gplay/res/values/setup.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - https://push-notifications.nextcloud.com - - 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com - https://nextcloud-a7dea.firebaseio.com - 829118773643 - AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s - 1:829118773643:android:512449826e931d0e - AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s - nextcloud-a7dea.appspot.com - nextcloud-a7dea - - - diff --git a/app/src/gplay/res/xml/preferences.xml b/app/src/gplay/res/xml/preferences.xml new file mode 100644 index 000000000000..63d3ecf6fa8e --- /dev/null +++ b/app/src/gplay/res/xml/preferences.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/huawei/java/com/ionos/analytics/FirebaseAnalyticsManager.kt b/app/src/huawei/java/com/ionos/analytics/FirebaseAnalyticsManager.kt new file mode 100644 index 000000000000..55e9d5dfa405 --- /dev/null +++ b/app/src/huawei/java/com/ionos/analytics/FirebaseAnalyticsManager.kt @@ -0,0 +1,12 @@ +package com.ionos.analytics + +import android.content.Context +import javax.inject.Inject + +//STUB +class FirebaseAnalyticsManager @Inject constructor( + private val context: Context, +) : AnalyticsManager { + override fun setEnabled(enabled: Boolean) { + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30f0dba91170..9b193e7b4980 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -391,6 +391,21 @@ android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="false" android:theme="@style/Theme.ownCloud.Media" /> + + + + + + + + + + + + + diff --git a/app/src/main/assets/branding/config.json b/app/src/main/assets/branding/config.json new file mode 100644 index 000000000000..482be3469eee --- /dev/null +++ b/app/src/main/assets/branding/config.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "loginUrls": { + "byCountry": {}, + "byLanguage": {}, + "default": "" + } +} diff --git a/app/src/main/java/com/ionos/analytics/AnalyticsManager.kt b/app/src/main/java/com/ionos/analytics/AnalyticsManager.kt new file mode 100644 index 000000000000..6221b5a227e6 --- /dev/null +++ b/app/src/main/java/com/ionos/analytics/AnalyticsManager.kt @@ -0,0 +1,12 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.analytics + +interface AnalyticsManager { + fun setEnabled(enabled: Boolean) +} diff --git a/app/src/main/java/com/ionos/annotation/IonosCustomization.kt b/app/src/main/java/com/ionos/annotation/IonosCustomization.kt new file mode 100644 index 000000000000..9b7730a1021c --- /dev/null +++ b/app/src/main/java/com/ionos/annotation/IonosCustomization.kt @@ -0,0 +1,24 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.annotation + +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.FIELD, + AnnotationTarget.CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.EXPRESSION, +) +@Retention(AnnotationRetention.SOURCE) +/* Used to highlight changes in core code +* Alternatives: +* - for layouts use - 'app:ionosCustomization=""' +* - comment with text '' where other options is not applicable +* */ +annotation class IonosCustomization(val value: String = "") \ No newline at end of file diff --git a/app/src/main/java/com/ionos/authorization_method/AuthorizationMethodActivity.kt b/app/src/main/java/com/ionos/authorization_method/AuthorizationMethodActivity.kt new file mode 100644 index 000000000000..aa9a91deb387 --- /dev/null +++ b/app/src/main/java/com/ionos/authorization_method/AuthorizationMethodActivity.kt @@ -0,0 +1,41 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.authorization_method + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.owncloud.android.authentication.AuthenticatorActivity +import com.owncloud.android.databinding.ViewAuthorizationMethodBinding + +class AuthorizationMethodActivity : AppCompatActivity() { + + companion object { + @JvmStatic + fun createInstance(context: Context) = Intent(context, AuthorizationMethodActivity::class.java) + } + + private val viewBinding by lazy { ViewAuthorizationMethodBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + initListeners() + } + + private fun initListeners() { + viewBinding.bLogin.setOnClickListener { login() } + } + + private fun login() { + val intent = Intent(this, AuthenticatorActivity::class.java) + startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/branding/BrandConfig.kt b/app/src/main/java/com/ionos/branding/BrandConfig.kt new file mode 100644 index 000000000000..ea98fd0a273a --- /dev/null +++ b/app/src/main/java/com/ionos/branding/BrandConfig.kt @@ -0,0 +1,38 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2026 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.branding + +import kotlinx.serialization.Serializable + +/** + * Per-brand build configuration, injected by the Brander tool before the build. + * + * The file lives in the brand payload directories, so the asset merger picks the right one for the + * built flavor (see doc/ADR-001-new-brand-variant.md): + * - `app/src/brandIonos/assets/branding/config.json` for the IONOS brand, + * - `app/src/brandStrato/assets/branding/config.json` for the HiDrive Next brand, + * - `app/src/main/assets/branding/config.json` as the neutral default for non-branded flavors. + * + * Every field has a default so that adding a key to the file never breaks the flavors that do not + * define it yet. + */ +@Serializable +data class BrandConfig( + val version: Int = 1, + val loginUrls: LoginUrls = LoginUrls() +) + +@Serializable +data class LoginUrls( + + val byCountry: Map = emptyMap(), + + val byLanguage: Map = emptyMap(), + + val default: String = "" +) diff --git a/app/src/main/java/com/ionos/branding/BrandConfigProvider.kt b/app/src/main/java/com/ionos/branding/BrandConfigProvider.kt new file mode 100644 index 000000000000..6d6e2d4a9878 --- /dev/null +++ b/app/src/main/java/com/ionos/branding/BrandConfigProvider.kt @@ -0,0 +1,43 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2026 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.branding + +import android.content.Context +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.serialization.json.Json +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Reads [BrandConfig] from the assets of the built flavor once and caches it. + * + * A missing or malformed file never crashes the app: it falls back to an empty configuration, which + * makes the callers use their own defaults. + */ +@Singleton +class BrandConfigProvider @Inject constructor(private val context: Context) { + + val config: BrandConfig by lazy { read() } + + private fun read(): BrandConfig = runCatching { + context.assets.open(ASSET_PATH).use { parse(it.reader().readText()) } + }.getOrElse { error -> + Log_OC.e(TAG, "Cannot read $ASSET_PATH, falling back to an empty config", error) + BrandConfig() + } + + companion object { + private const val TAG = "BrandConfigProvider" + private const val ASSET_PATH = "branding/config.json" + + private val json = Json { ignoreUnknownKeys = true } + + /** Kept separate from the asset access so that it can be covered by plain JVM tests. */ + fun parse(raw: String): BrandConfig = json.decodeFromString(raw) + } +} diff --git a/app/src/main/java/com/ionos/di/StratoModule.kt b/app/src/main/java/com/ionos/di/StratoModule.kt new file mode 100644 index 000000000000..b80986682c5d --- /dev/null +++ b/app/src/main/java/com/ionos/di/StratoModule.kt @@ -0,0 +1,35 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.di + +import com.ionos.analytics.AnalyticsManager +import com.ionos.privacy.DataProtectionActivity +import com.ionos.analytics.FirebaseAnalyticsManager +import com.ionos.player.PlayerModule +import com.ionos.privacy.PrivacySettingsActivity +import com.ionos.scanbot.di.NCScanbotModule +import com.ionos.startup.IonosInitializer +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module(includes = [NCScanbotModule::class, PlayerModule::class]) +abstract class StratoModule { + + @ContributesAndroidInjector + abstract fun ionosInitializer(): IonosInitializer + + @ContributesAndroidInjector + abstract fun dataProtectionActivity(): DataProtectionActivity + + @ContributesAndroidInjector + abstract fun privacySettingsActivity(): PrivacySettingsActivity + + @Binds + abstract fun analyticsManager(firebaseAnalyticsManager: FirebaseAnalyticsManager): AnalyticsManager +} diff --git a/app/src/main/java/com/ionos/player/PlayerModule.kt b/app/src/main/java/com/ionos/player/PlayerModule.kt new file mode 100644 index 000000000000..b1261dd2b6b4 --- /dev/null +++ b/app/src/main/java/com/ionos/player/PlayerModule.kt @@ -0,0 +1,100 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player + +import android.content.Context +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.cache.Cache +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import com.ionos.player.media3.PlaybackModelImpl +import com.ionos.player.media3.PlaybackService +import com.ionos.player.media3.common.PlayerFactory +import com.ionos.player.media3.exoplayer.ExoPlayerFactory +import com.ionos.player.media3.session.MediaSessionHolder +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.error_strategy.DefaultPlaybackErrorStrategy +import com.ionos.player.model.error_strategy.PlaybackErrorStrategy +import com.ionos.player.ui.PlayerActivity +import com.ionos.player.ui.audio.AudioFileFragment +import com.ionos.player.ui.audio.AudioPlayerView +import com.ionos.player.ui.common.PlayerProgressIndicator +import com.ionos.player.ui.control.PlayerControlView +import com.ionos.player.ui.video.VideoFileFragment +import com.ionos.player.ui.video.VideoPlayerView +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.android.ContributesAndroidInjector +import java.io.File +import javax.inject.Singleton + +@Module +abstract class PlayerModule { + + companion object { + private const val PLAYER_CACHE_DIR_NAME = "player" + private const val PLAYER_CACHE_SIZE = 300 * 1024 * 1024L + + @UnstableApi + @Singleton + @Provides + fun provideCache(context: Context): Cache { + return SimpleCache( + File(context.cacheDir, PLAYER_CACHE_DIR_NAME), + LeastRecentlyUsedCacheEvictor(PLAYER_CACHE_SIZE) + ) + } + } + + @Binds + @Singleton + abstract fun bindPlaybackModel( + model: PlaybackModelImpl, + ): PlaybackModel + + @Binds + @Singleton + abstract fun mediaSessionHolder( + playbackModel: PlaybackModelImpl, + ): MediaSessionHolder + + @Binds + abstract fun bindPlayerFactory( + playerFactory: ExoPlayerFactory, + ): PlayerFactory + + @Binds + abstract fun bindPlaybackErrorStrategy( + strategy: DefaultPlaybackErrorStrategy, + ): PlaybackErrorStrategy + + @ContributesAndroidInjector + abstract fun playbackService(): PlaybackService + + @ContributesAndroidInjector + abstract fun playerActivity(): PlayerActivity + + @ContributesAndroidInjector + abstract fun audioPlayerView(): AudioPlayerView + + @ContributesAndroidInjector + abstract fun videoPlayerView(): VideoPlayerView + + @ContributesAndroidInjector + abstract fun playerControlView(): PlayerControlView + + @ContributesAndroidInjector + abstract fun playerProgressIndicator(): PlayerProgressIndicator + + @ContributesAndroidInjector + abstract fun audioFileFragment(): AudioFileFragment + + @ContributesAndroidInjector + abstract fun videoFileFragment(): VideoFileFragment +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/media3/MediaNotificationProvider.kt b/app/src/main/java/com/ionos/player/media3/MediaNotificationProvider.kt new file mode 100644 index 000000000000..1eb86be8ddf6 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/MediaNotificationProvider.kt @@ -0,0 +1,26 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3 + +import android.content.Context +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.DefaultMediaNotificationProvider +import com.ionos.player.media3.common.playbackFile + +@UnstableApi +class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) { + + override fun getNotificationContentTitle(metadata: MediaMetadata): CharSequence? { + return if (metadata.title.isNullOrEmpty()) { + metadata.playbackFile?.getNameWithoutExtension() + } else { + metadata.title + } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/PlaybackModelImpl.kt b/app/src/main/java/com/ionos/player/media3/PlaybackModelImpl.kt new file mode 100644 index 000000000000..5c2ca239b143 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/PlaybackModelImpl.kt @@ -0,0 +1,228 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3 + +import android.content.Context +import android.view.SurfaceView +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaController +import androidx.media3.session.MediaSession +import com.ionos.player.media3.common.MediaItemFactory +import com.ionos.player.media3.common.playbackFile +import com.ionos.player.media3.controller.MediaControllerFactory +import com.ionos.player.media3.controller.indexOfFirst +import com.ionos.player.media3.controller.setRepeatMode +import com.ionos.player.media3.controller.updateMediaItems +import com.ionos.player.media3.session.MediaSessionFactory +import com.ionos.player.media3.session.MediaSessionHolder +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackFiles +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.PlaybackModelCompositeListener +import com.ionos.player.model.PlaybackSettings +import com.ionos.player.model.error_strategy.PlaybackErrorStrategy +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.RepeatMode +import com.ionos.player.util.PeriodicAction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.Optional +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PlaybackModelImpl @Inject constructor( + private val context: Context, + private val mediaSessionFactory: MediaSessionFactory, + private val mediaItemFactory: MediaItemFactory, + private val playbackSettings: PlaybackSettings, + private val playbackErrorStrategy: PlaybackErrorStrategy, +) : PlaybackModel, MediaSessionHolder { + + companion object { + private const val CHECK_PROGRESS_INTERVAL = 1000 + } + + private val stateFactory = PlaybackStateFactory() + private val compositeListener = PlaybackModelCompositeListener() + + private val checkProgressPeriodicAction = PeriodicAction(CHECK_PROGRESS_INTERVAL) { + state.ifPresent(compositeListener::onPlaybackUpdate) + } + + private val playerListener = PlaybackModelPlayerListener( + checkProgressPeriodicAction, + this::onPlaybackUpdate, + this::onPlaybackError, + ) + + private val controllerListener = object : MediaController.Listener { + override fun onDisconnected(controller: MediaController) { + controller.removeListener(playerListener) + controllerScope?.cancel() + checkProgressPeriodicAction.stop() + state.ifPresent(compositeListener::onPlaybackUpdate) + } + } + + private val controllerFactory = MediaControllerFactory(controllerListener) + private var controllerScope: CoroutineScope? = null + private var controller: MediaController? = null + + private var mediaSession: MediaSession? = null + + override val state: Optional + get() { + return stateFactory.create(controller) + } + + @UnstableApi + override fun getMediaSession(): MediaSession { + return mediaSession ?: mediaSessionFactory.create().also { + mediaSession = it + } + } + + override suspend fun start() { + controller = controllerFactory.create(context).apply { + addListener(playerListener) + setRepeatMode(playbackSettings.repeatMode) + shuffleModeEnabled = playbackSettings.isShuffle + controllerScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + } + } + + override fun setFilesFlow(filesFlow: Flow) { + controllerScope?.launch { + filesFlow + .catch { + compositeListener.onPlaybackError(it) + release() + } + .collectLatest { setFiles(it) } + } + } + + override fun setFiles(files: PlaybackFiles) { + if (files.list.isEmpty()) { + release() + return + } + + val currentFile = controller?.currentMediaItem?.mediaMetadata?.playbackFile + + controller?.let { controller -> + val mediaItems = files.list.map(mediaItemFactory::create) + + if (currentFile == null) { + controller.setMediaItems(mediaItems) + } else if (files.list.any { it.id == currentFile.id }) { + controller.updateMediaItems(mediaItems) + } else { + val nextFileIndex = (files.list + currentFile) + .sortedWith(files.comparator) + .indexOfFirst { it.id == currentFile.id } + .let { if (it in 0..files.list.lastIndex) it else 0 } + controller.setMediaItems(mediaItems, nextFileIndex, 0) + } + + controller.prepare() + } + } + + override fun release() { + controller?.release() + mediaSession?.player?.release() + mediaSession?.release() + mediaSession = null + } + + override fun setVideoSurfaceView(surfaceView: SurfaceView?) { + controller?.setVideoSurfaceView(surfaceView) + } + + override fun addListener(listener: PlaybackModel.Listener) { + compositeListener.addListener(listener) + } + + override fun removeListener(listener: PlaybackModel.Listener) { + compositeListener.removeListener(listener) + } + + override fun play() { + controller?.run { + prepare() + play() + } + } + + override fun pause() { + controller?.pause() + } + + override fun stop() { + controller?.stop() + } + + override fun playNext() { + controller?.run { + seekToNextMediaItem() + prepare() + } + } + + override fun playPrevious() { + controller?.run { + seekToPreviousMediaItem() + prepare() + } + } + + override fun seekToPosition(positionInMilliseconds: Int) { + controller?.seekTo(positionInMilliseconds.toLong()) + } + + override fun setRepeatMode(repeatMode: RepeatMode) { + playbackSettings.setRepeatMode(repeatMode) + controller?.setRepeatMode(repeatMode) + } + + override fun setShuffle(shuffle: Boolean) { + playbackSettings.setShuffle(shuffle) + controller?.shuffleModeEnabled = shuffle + } + + override fun switchToFile(file: PlaybackFile) { + controller?.run { + val mediaItemIndex = indexOfFirst { it.mediaId == file.id } + if (mediaItemIndex >= 0 && mediaItemIndex != currentMediaItemIndex) { + seekToDefaultPosition(mediaItemIndex) + prepare() + } + } + } + + private fun onPlaybackUpdate() { + state.ifPresent(compositeListener::onPlaybackUpdate) + } + + private fun onPlaybackError(error: Throwable) { + compositeListener.onPlaybackError(error) + state.ifPresent { state -> + if (playbackErrorStrategy.switchToNextSource(error, state)) { + playNext() + } + } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/PlaybackModelPlayerListener.kt b/app/src/main/java/com/ionos/player/media3/PlaybackModelPlayerListener.kt new file mode 100644 index 000000000000..182d4fbfa937 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/PlaybackModelPlayerListener.kt @@ -0,0 +1,89 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3 + +import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException +import androidx.media3.exoplayer.ExoPlaybackException +import androidx.media3.exoplayer.source.UnrecognizedInputFormatException +import com.ionos.player.model.SourceException +import com.ionos.player.util.PeriodicAction + +class PlaybackModelPlayerListener( + private val checkProgressPeriodicAction: PeriodicAction, + private val onPlaybackUpdate: () -> Unit, + private val onPlaybackError: (Throwable) -> Unit, +) : Player.Listener { + + companion object { + private const val BROKEN_SOURCE_ERROR_CODE: Int = 416 + } + + override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { + onPlaybackUpdate() + } + + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + onPlaybackUpdate() + } + + override fun onTracksChanged(tracks: Tracks) { + onPlaybackUpdate() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + onPlaybackUpdate() + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + onPlaybackUpdate() + if (isPlaying) { + checkProgressPeriodicAction.start() + } else { + checkProgressPeriodicAction.stop() + } + } + + override fun onRepeatModeChanged(repeatMode: Int) { + onPlaybackUpdate() + } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + onPlaybackUpdate() + } + + override fun onVideoSizeChanged(videoSize: VideoSize) { + onPlaybackUpdate() + } + + @UnstableApi + override fun onPlayerError(error: PlaybackException) { + if (error is ExoPlaybackException && error.type == ExoPlaybackException.TYPE_SOURCE) { + onPlaybackError(error.toSourceException()) + } else { + onPlaybackError(error) + } + } + + @UnstableApi + private fun ExoPlaybackException.toSourceException(): SourceException { + return if (sourceException is InvalidResponseCodeException) { + SourceException((sourceException as InvalidResponseCodeException).responseCode) + } else if (cause != null && cause is UnrecognizedInputFormatException) { + SourceException(BROKEN_SOURCE_ERROR_CODE) + } else { + SourceException() + } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/PlaybackService.kt b/app/src/main/java/com/ionos/player/media3/PlaybackService.kt new file mode 100644 index 000000000000..4210eb893981 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/PlaybackService.kt @@ -0,0 +1,75 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3 + +import android.content.Intent +import android.os.IBinder +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ControllerInfo +import androidx.media3.session.MediaSessionService +import com.ionos.player.media3.session.MediaSessionActivityFactory +import com.ionos.player.media3.session.MediaSessionHolder +import dagger.android.AndroidInjection +import javax.inject.Inject + +class PlaybackService : MediaSessionService() { + + @Inject + lateinit var mediaSessionHolder: MediaSessionHolder + + @Inject + lateinit var mediaSessionActivityFactory: MediaSessionActivityFactory + + private var bindingCount: Int = 0 + + @UnstableApi + override fun onCreate() { + super.onCreate() + AndroidInjection.inject(this) + setMediaNotificationProvider(MediaNotificationProvider(this)) + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaSession? { + return mediaSessionHolder.getMediaSession() + } + + @UnstableApi + override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) { + val currentMediaItem = session.player.currentMediaItem + mediaSessionActivityFactory.create(currentMediaItem)?.let(session::setSessionActivity) + super.onUpdateNotification(session, startInForegroundRequired) + } + + override fun onBind(intent: Intent?): IBinder? { + val result = super.onBind(intent) + if (result != null) { + bindingCount++ + } + return result + } + + override fun onUnbind(intent: Intent?): Boolean { + bindingCount-- + if (bindingCount == 0) { + stopSelf() + } + return super.onUnbind(intent) + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + mediaSessionHolder.release() + stopSelf() + } + + override fun onDestroy() { + mediaSessionHolder.release() + super.onDestroy() + } +} diff --git a/app/src/main/java/com/ionos/player/media3/PlaybackStateFactory.kt b/app/src/main/java/com/ionos/player/media3/PlaybackStateFactory.kt new file mode 100644 index 000000000000..abc94238e690 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/PlaybackStateFactory.kt @@ -0,0 +1,89 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3 + +import androidx.media3.common.Player +import com.ionos.player.media3.common.playbackFile +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.state.PlaybackItemMetadata +import com.ionos.player.model.state.PlaybackItemState +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.PlayerState +import com.ionos.player.model.state.RepeatMode +import com.ionos.player.model.state.VideoSize +import java.util.Optional + +class PlaybackStateFactory() { + + fun create(player: Player?): Optional { + if (player == null) { + return Optional.empty() + } + val state = PlaybackState( + currentFiles = player.getCurrentFiles(), + currentItemState = player.getCurrentItemState(), + repeatMode = player.mapRepeatMode(), + shuffle = player.shuffleModeEnabled, + ) + return Optional.of(state) + } + + private fun Player.getCurrentFiles(): List { + return buildList { + for (i in 0 until mediaItemCount) { + val mediaItem = getMediaItemAt(i) + val playbackFile = mediaItem.mediaMetadata.playbackFile + playbackFile?.let(::add) + } + } + } + + private fun Player.getCurrentItemState(): PlaybackItemState? { + val currentFile = currentMediaItem?.mediaMetadata?.playbackFile + return currentFile?.let { getCurrentItemState(it) } + } + + private fun Player.getCurrentItemState(currentFile: PlaybackFile) = PlaybackItemState( + file = currentFile, + playerState = mapPlayerState(), + metadata = if (mediaMetadata.playbackFile?.id == currentFile.id) mapMetadata(currentFile) else null, + videoSize = mapVideoSize(), + currentTimeInMilliseconds = currentPosition.toInt(), + maxTimeInMilliseconds = duration.toInt(), + ) + + private fun Player.mapPlayerState(): PlayerState = when (playbackState) { + Player.STATE_IDLE -> PlayerState.IDLE + Player.STATE_ENDED -> PlayerState.COMPLETED + Player.STATE_BUFFERING, Player.STATE_READY -> if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED + else -> PlayerState.NONE + } + + private fun Player.mapMetadata(currentFile: PlaybackFile) = PlaybackItemMetadata( + title = mediaMetadata.title ?: currentFile.getNameWithoutExtension(), + artist = mediaMetadata.artist, + album = mediaMetadata.albumTitle, + genre = mediaMetadata.genre, + year = mediaMetadata.recordingYear, + description = mediaMetadata.description, + artworkData = mediaMetadata.artworkData, + artworkUri = mediaMetadata.artworkUri?.toString(), + ) + + private fun Player.mapVideoSize(): VideoSize? { + return videoSize + .takeIf { it.width > 0 && it.height > 0 } + ?.let { VideoSize(width = it.width, height = it.height) } + } + + private fun Player.mapRepeatMode(): RepeatMode = when (repeatMode) { + Player.REPEAT_MODE_ONE -> RepeatMode.SINGLE + Player.REPEAT_MODE_ALL -> RepeatMode.ALL + else -> RepeatMode.OFF + } +} diff --git a/app/src/main/java/com/ionos/player/media3/common/MediaItemFactory.kt b/app/src/main/java/com/ionos/player/media3/common/MediaItemFactory.kt new file mode 100644 index 000000000000..e53c5bee7a53 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/common/MediaItemFactory.kt @@ -0,0 +1,33 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.common + +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import com.ionos.player.model.PlaybackFile +import javax.inject.Inject + +class MediaItemFactory @Inject constructor() { + + fun create(file: PlaybackFile): MediaItem { + return MediaItem + .Builder() + .setMediaId(file.id) + .setUri(file.uri) + .setMediaMetadata(createMetadata(file)) + .setMimeType(file.mimeType) + .build() + } + + private fun createMetadata(file: PlaybackFile): MediaMetadata { + return MediaMetadata + .Builder() + .setExtras(file) + .build() + } +} diff --git a/app/src/main/java/com/ionos/player/media3/common/MediaMetadata.kt b/app/src/main/java/com/ionos/player/media3/common/MediaMetadata.kt new file mode 100644 index 000000000000..f71b9d406c65 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/common/MediaMetadata.kt @@ -0,0 +1,21 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.common + +import android.os.Bundle +import androidx.media3.common.MediaMetadata +import com.ionos.player.model.PlaybackFile + +private const val PLAYBACK_FILE_KEY = "playback_file" + +fun MediaMetadata.Builder.setExtras(playbackFile: PlaybackFile): MediaMetadata.Builder { + return setExtras(Bundle().apply { putSerializable(PLAYBACK_FILE_KEY, playbackFile) }) +} + +val MediaMetadata.playbackFile: PlaybackFile? + get() = extras?.getSerializable(PLAYBACK_FILE_KEY) as? PlaybackFile diff --git a/app/src/main/java/com/ionos/player/media3/common/PlayerFactory.kt b/app/src/main/java/com/ionos/player/media3/common/PlayerFactory.kt new file mode 100644 index 000000000000..f0cddde1d5d6 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/common/PlayerFactory.kt @@ -0,0 +1,14 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.common + +import androidx.media3.common.Player + +interface PlayerFactory { + fun create(): Player +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/media3/controller/MediaController.kt b/app/src/main/java/com/ionos/player/media3/controller/MediaController.kt new file mode 100644 index 000000000000..82efcdf43562 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/controller/MediaController.kt @@ -0,0 +1,61 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.controller + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import com.ionos.player.model.state.RepeatMode + +fun MediaController.indexOfFirst(satisfies: (MediaItem) -> Boolean): Int { + for (index in 0..) { + val oldCurrentMediaItemIndex = currentMediaItemIndex + .takeIf { it >= 0 } + + val newCurrentMediaItemIndex = currentMediaItem + ?.mediaId + ?.let { currentMediaId -> newMediaItems.indexOfFirst { it.mediaId == currentMediaId } } + ?.takeIf { it >= 0 } + + if (oldCurrentMediaItemIndex != null && newCurrentMediaItemIndex != null) { + if (oldCurrentMediaItemIndex < mediaItemCount - 1) { + removeMediaItems(oldCurrentMediaItemIndex + 1, mediaItemCount) + } + if (newCurrentMediaItemIndex < newMediaItems.size - 1) { + val itemsToAdd = newMediaItems.subList(newCurrentMediaItemIndex + 1, newMediaItems.size) + addMediaItems(itemsToAdd) + } + if (oldCurrentMediaItemIndex > 0) { + removeMediaItems(0, oldCurrentMediaItemIndex) + } + if (newCurrentMediaItemIndex > 0) { + val itemsToAdd = newMediaItems.subList(0, newCurrentMediaItemIndex) + addMediaItems(0, itemsToAdd) + } + replaceMediaItem(newCurrentMediaItemIndex, newMediaItems[newCurrentMediaItemIndex]) + } else { + setMediaItems(newMediaItems) + } +} + +fun MediaController.setRepeatMode(mode: RepeatMode) { + repeatMode = when (mode) { + RepeatMode.SINGLE -> Player.REPEAT_MODE_ONE + RepeatMode.ALL -> Player.REPEAT_MODE_ALL + RepeatMode.OFF -> Player.REPEAT_MODE_OFF + } +} diff --git a/app/src/main/java/com/ionos/player/media3/controller/MediaControllerFactory.kt b/app/src/main/java/com/ionos/player/media3/controller/MediaControllerFactory.kt new file mode 100644 index 000000000000..8f1beeaae761 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/controller/MediaControllerFactory.kt @@ -0,0 +1,29 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.controller + +import android.content.ComponentName +import android.content.Context +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.ionos.player.media3.PlaybackService +import kotlinx.coroutines.guava.await + +class MediaControllerFactory( + private val controllerListener: MediaController.Listener, +) { + + suspend fun create(context: Context): MediaController { + val token = SessionToken(context, ComponentName(context, PlaybackService::class.java)) + return MediaController + .Builder(context, token) + .setListener(controllerListener) + .buildAsync() + .await() + } +} diff --git a/app/src/main/java/com/ionos/player/media3/datasource/DataSourceFactory.kt b/app/src/main/java/com/ionos/player/media3/datasource/DataSourceFactory.kt new file mode 100644 index 000000000000..a1ecc17ccb85 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/datasource/DataSourceFactory.kt @@ -0,0 +1,54 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.datasource + +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.FileDataSource +import androidx.media3.datasource.cache.Cache +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.CacheKeyFactory +import androidx.media3.datasource.okhttp.OkHttpDataSource +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import javax.inject.Inject + +class DataSourceFactory @Inject constructor( + private val fileDataStorageManager: FileDataStorageManager, + private val clientFactory: ClientFactory, + private val accountManager: UserAccountManager, + @UnstableApi private val cache: Cache, +) : DataSource.Factory { + + @UnstableApi + override fun createDataSource(): DataSource { + return CacheDataSource.Factory() + .setUpstreamDataSourceFactory(createStreamDataSourceFactory()) + .setCache(cache) + .setCacheKeyFactory(CacheKeyFactory.DEFAULT) + .createDataSource() + } + + @UnstableApi + private fun createStreamDataSourceFactory() = DataSource.Factory { + StreamDataSource( + fileDataStorageManager = fileDataStorageManager, + ownCloudClient = clientFactory.create(accountManager.user), + fileDataSource = FileDataSource.Factory().createDataSource(), + httpDataSource = createHttpDataSource(), + ) + } + + @UnstableApi + private fun createHttpDataSource() = OkHttpDataSource + .Factory(clientFactory.createNextcloudClient(accountManager.user).client) + .setUserAgent(MainApp.getUserAgent()) + .createDataSource() +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/media3/datasource/StreamDataSource.kt b/app/src/main/java/com/ionos/player/media3/datasource/StreamDataSource.kt new file mode 100644 index 000000000000..94be0b1e75fd --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/datasource/StreamDataSource.kt @@ -0,0 +1,80 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.datasource + +import android.net.Uri +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.TransferListener +import com.ionos.player.model.getRemoteFileId +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.files.StreamMediaFileOperation +import com.owncloud.android.lib.common.OwnCloudClient +import java.io.IOException + +internal class StreamDataSource( + private val fileDataStorageManager: FileDataStorageManager, + private val ownCloudClient: OwnCloudClient, + private val fileDataSource: DataSource, + private val httpDataSource: DataSource, +) : DataSource { + private var currentDataSource: DataSource? = null + + @UnstableApi + override fun addTransferListener(listener: TransferListener) { + fileDataSource.addTransferListener(listener) + httpDataSource.addTransferListener(listener) + } + + @UnstableApi + override fun open(dataSpec: DataSpec): Long { + val fileId = dataSpec.uri.getRemoteFileId() ?: throw IllegalArgumentException("Invalid URI: ${dataSpec.uri}") + val file = fileDataStorageManager.getFileByLocalId(fileId) + + return if (file != null && file.isDown) { + val uri = file.storageUri + currentDataSource = fileDataSource + fileDataSource.open(dataSpec.buildUpon(uri)) + } else { + val sfo = StreamMediaFileOperation(fileId) + val result = sfo.execute(ownCloudClient) + if (result.isSuccess) { + val uri = Uri.parse(result.data[0] as String) + currentDataSource = httpDataSource + httpDataSource.open(dataSpec.buildUpon(uri)) + } else { + throw IOException("Failed to retrieve streaming uri", result.exception) + } + } + } + + @UnstableApi + override fun getUri(): Uri? { + return currentDataSource?.uri + } + + @UnstableApi + override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int { + return currentDataSource?.read(buffer, offset, readLength) ?: throw IOException("DataSource not opened") + } + + @UnstableApi + override fun close() { + fileDataSource.close() + httpDataSource.close() + currentDataSource = null + } + + @UnstableApi + private fun DataSpec.buildUpon(uri: Uri): DataSpec { + return buildUpon() + .setUri(uri) + .build() + } +} diff --git a/app/src/main/java/com/ionos/player/media3/exoplayer/ExoPlayerFactory.kt b/app/src/main/java/com/ionos/player/media3/exoplayer/ExoPlayerFactory.kt new file mode 100644 index 000000000000..244a37ec7de8 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/exoplayer/ExoPlayerFactory.kt @@ -0,0 +1,36 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.exoplayer + +import android.content.Context +import androidx.media3.common.AudioAttributes +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import com.ionos.player.media3.common.PlayerFactory +import javax.inject.Inject + +class ExoPlayerFactory @Inject constructor( + private val context: Context, + private val mediaSourceFactory: MediaSourceFactory, +) : PlayerFactory { + + @UnstableApi + override fun create(): Player { + val renderersFactory = DefaultRenderersFactory(context) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + return ExoPlayer + .Builder(context, renderersFactory) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setTrackSelector(DefaultTrackSelector(context)) + .setMediaSourceFactory(mediaSourceFactory) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/media3/exoplayer/MediaSourceFactory.kt b/app/src/main/java/com/ionos/player/media3/exoplayer/MediaSourceFactory.kt new file mode 100644 index 000000000000..dbc4db80b7d3 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/exoplayer/MediaSourceFactory.kt @@ -0,0 +1,46 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.exoplayer + +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import androidx.media3.extractor.DefaultExtractorsFactory +import com.ionos.player.media3.datasource.DataSourceFactory +import javax.inject.Inject + +class MediaSourceFactory @Inject constructor( + private val dataSourceFactory: DataSourceFactory, +) : MediaSource.Factory { + + @UnstableApi + override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { + return this + } + + @UnstableApi + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory { + return this + } + + @UnstableApi + override fun getSupportedTypes(): IntArray { + return intArrayOf(C.CONTENT_TYPE_OTHER) + } + + @UnstableApi + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + return ProgressiveMediaSource + .Factory(dataSourceFactory, DefaultExtractorsFactory()) + .createMediaSource(mediaItem) + } +} diff --git a/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfig.kt b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfig.kt new file mode 100644 index 000000000000..a807c665d5f9 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfig.kt @@ -0,0 +1,18 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.resumption + +import com.ionos.player.model.PlaybackFileType +import com.owncloud.android.ui.fragment.SearchType + +data class PlaybackResumptionConfig( + val currentFileId: String, + val folderId: Long, + val fileType: PlaybackFileType, + val searchType: SearchType?, +) diff --git a/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfigStore.kt b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfigStore.kt new file mode 100644 index 000000000000..2b4949be5ca5 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionConfigStore.kt @@ -0,0 +1,71 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.resumption + +import android.content.Context +import androidx.core.content.edit +import com.ionos.player.model.PlaybackFileType +import com.owncloud.android.ui.fragment.SearchType +import javax.inject.Inject + +class PlaybackResumptionConfigStore @Inject constructor( + private val context: Context, +) { + companion object { + private const val PREFERENCES_FILE_NAME = "playback_resumption_config" + private const val CURRENT_FILE_ID_KEY = "current_file_id" + private const val FOLDER_ID_KEY = "folder_id" + private const val FILE_TYPE_KEY = "file_type" + private const val SEARCH_TYPE_KEY = "search_type" + } + + private val preferences by lazy { + context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) + } + + fun loadConfig(): PlaybackResumptionConfig? { + val currentFileId = preferences.getString(CURRENT_FILE_ID_KEY, null) + val folderId = preferences.getLong(FOLDER_ID_KEY, 0L) + val fileType = preferences.getString(FILE_TYPE_KEY, null)?.let(::PlaybackFileType) + val searchType = preferences.getString(SEARCH_TYPE_KEY, null)?.let(::SearchType) + return if (currentFileId != null && folderId != 0L && fileType != null) { + PlaybackResumptionConfig(currentFileId, folderId, fileType, searchType) + } else { + null + } + } + + fun saveConfig(currentFileId: String, folderId: Long, fileType: PlaybackFileType, searchType: SearchType?) { + preferences.edit { + putString(CURRENT_FILE_ID_KEY, currentFileId) + putLong(FOLDER_ID_KEY, folderId) + putString(FILE_TYPE_KEY, fileType.value) + putString(SEARCH_TYPE_KEY, searchType?.name) + } + } + + fun updateCurrentFileId(currentFileId: String) { + preferences.edit { + putString(CURRENT_FILE_ID_KEY, currentFileId) + } + } + + fun clear() { + preferences.edit { + clear() + } + } + + private fun PlaybackFileType(value: String): PlaybackFileType? { + return PlaybackFileType.entries.firstOrNull { it.value == value } + } + + private fun SearchType(name: String): SearchType? { + return SearchType.entries.firstOrNull { it.name == name } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionPlayerListener.kt b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionPlayerListener.kt new file mode 100644 index 000000000000..38932e99a5d1 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/resumption/PlaybackResumptionPlayerListener.kt @@ -0,0 +1,21 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.resumption + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import javax.inject.Inject + +class PlaybackResumptionPlayerListener @Inject constructor( + private val playbackResumptionConfigStore: PlaybackResumptionConfigStore, +) : Player.Listener { + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + mediaItem?.let { playbackResumptionConfigStore.updateCurrentFileId(it.mediaId) } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/session/MediaSessionActivityFactory.kt b/app/src/main/java/com/ionos/player/media3/session/MediaSessionActivityFactory.kt new file mode 100644 index 000000000000..cc8056c30f17 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/session/MediaSessionActivityFactory.kt @@ -0,0 +1,39 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.session + +import android.app.PendingIntent +import android.content.Context +import androidx.media3.common.MediaItem +import com.ionos.player.media3.common.playbackFile +import com.ionos.player.model.PlaybackFileType +import com.ionos.player.ui.PlayerActivity +import com.ionos.player.util.SystemVersion +import javax.inject.Inject + +class MediaSessionActivityFactory @Inject constructor( + private val context: Context, +) { + + fun create(currentMediaItem: MediaItem?): PendingIntent? { + val currentFile = currentMediaItem?.mediaMetadata?.playbackFile ?: return null + val fileType = PlaybackFileType.entries + .firstOrNull { currentFile.mimeType.startsWith(it.value, ignoreCase = true) } + ?: throw IllegalArgumentException("Unsupported file type: ${currentFile.mimeType}") + + val intent = PlayerActivity.createIntent(context, fileType) + + val requestCode = System.currentTimeMillis().toInt() + + return if (SystemVersion.greaterOrEqualToS()) { + PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_IMMUTABLE) + } else { + PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + } +} diff --git a/app/src/main/java/com/ionos/player/media3/session/MediaSessionBitmapLoader.kt b/app/src/main/java/com/ionos/player/media3/session/MediaSessionBitmapLoader.kt new file mode 100644 index 000000000000..e2a239661320 --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/session/MediaSessionBitmapLoader.kt @@ -0,0 +1,111 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.session + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.BitmapLoader +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSourceBitmapLoader +import com.google.common.util.concurrent.ListenableFuture +import com.ionos.player.media3.common.playbackFile +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.ThumbnailLoader +import com.owncloud.android.R +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.guava.future + +@UnstableApi +class MediaSessionBitmapLoader( + private val context: Context, + private val thumbnailLoader: ThumbnailLoader, + private val delegate: BitmapLoader = DataSourceBitmapLoader(context), +) : BitmapLoader by delegate { + + companion object { + private const val THUMBNAIL_TARGET_SIZE = 160 + private const val LARGE_THUMBNAIL_TARGET_SIZE = 320 + } + + private val thumbnailSize: Int = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) LARGE_THUMBNAIL_TARGET_SIZE + else THUMBNAIL_TARGET_SIZE + + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private var previousRequest: BitmapRequest? = null + + override fun loadBitmapFromMetadata(metadata: MediaMetadata): ListenableFuture? { + val file = metadata.playbackFile + val previousRequest = this.previousRequest + + if (previousRequest != null && + previousRequest.mediaId == file?.id && + previousRequest.artworkData.contentEquals(metadata.artworkData) && + previousRequest.artworkUri == metadata.artworkUri + ) { + return previousRequest.bitmapFuture + } + + val bitmapFuture = coroutineScope.future { + getBitmapFromMetadata(metadata, file?.id) + ?: file?.let { getBitmapForFile(it) } + ?: getDefaultBitmap(file) + } + + this.previousRequest = BitmapRequest( + file?.id, + metadata.artworkData, + metadata.artworkUri, + bitmapFuture, + ) + + return bitmapFuture + } + + private suspend fun getBitmapFromMetadata(metadata: MediaMetadata, fileId: String?): Bitmap? { + val model = metadata.artworkData ?: metadata.artworkUri ?: return null + return try { + thumbnailLoader.load(context, model, fileId, thumbnailSize, thumbnailSize) + } catch (e: Exception) { + null + } + } + + private suspend fun getBitmapForFile(file: PlaybackFile): Bitmap? { + return try { + thumbnailLoader.load(context, file, thumbnailSize, thumbnailSize) + } catch (e: Exception) { + null + } + } + + private fun getDefaultBitmap(file: PlaybackFile?): Bitmap { + val drawable = if (file != null && MimeTypeUtil.isVideo(file.mimeType)) { + ContextCompat.getDrawable(context, R.drawable.player_ic_notification_video) + } else { + ContextCompat.getDrawable(context, R.drawable.player_ic_notification_audio) + } + return drawable?.toBitmap() ?: throw IllegalStateException("Could not decode resource") + } + + private class BitmapRequest( + val mediaId: String?, + val artworkData: ByteArray?, + val artworkUri: Uri?, + val bitmapFuture: ListenableFuture, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/media3/session/MediaSessionCallback.kt b/app/src/main/java/com/ionos/player/media3/session/MediaSessionCallback.kt new file mode 100644 index 000000000000..a7597da9488a --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/session/MediaSessionCallback.kt @@ -0,0 +1,119 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.session + +import android.os.Bundle +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ConnectionResult +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.ionos.player.media3.common.MediaItemFactory +import com.ionos.player.media3.resumption.PlaybackResumptionConfigStore +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackFilesRepository +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.getPlaybackUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.guava.future +import kotlinx.coroutines.withContext +import java.util.concurrent.CancellationException +import javax.inject.Inject +import javax.inject.Provider + +class MediaSessionCallback @Inject constructor( + private val playbackResumptionConfigStore: PlaybackResumptionConfigStore, + private val playbackFilesRepository: PlaybackFilesRepository, + private val mediaItemFactory: MediaItemFactory, + private val playbackModelProvider: Provider, +) : MediaSession.Callback { + private val playbackModel get() = playbackModelProvider.get() + + companion object { + const val CLOSE_ACTION = "CLOSE_ACTION" + } + + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val sessionCommandsBuilder = connectionResult.availableSessionCommands.buildUpon() + sessionCommandsBuilder.add(SessionCommand(CLOSE_ACTION, Bundle.EMPTY)) + val sessionCommands = sessionCommandsBuilder.build() + return ConnectionResult.accept(sessionCommands, connectionResult.availablePlayerCommands) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == CLOSE_ACTION) { + playbackModel.release() + } + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + @UnstableApi + override fun onPlaybackResumption( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo + ): ListenableFuture { + return GlobalScope.future { + try { + val (currentFileId, folderId, fileType, searchType) = playbackResumptionConfigStore.loadConfig() + ?: throw IllegalStateException("Playback resumption config is null") + val playbackFilesFlow = playbackFilesRepository.observe(folderId, fileType, searchType) + val playbackFiles = playbackFilesFlow.first().list.ifEmpty { + throw IllegalStateException("Playback files are empty") + } + withContext(Dispatchers.Main) { + playbackModel.start() + playbackModel.setFilesFlow(playbackFilesFlow.drop(1)) + } + playbackFiles.toMediaItemsWithStartPosition(currentFileId) + } catch (t: Throwable) { + if (t is CancellationException) throw t + val stubPlaybackFile = getStubPlaybackFile() + val stubPlaybackFiles = listOf(stubPlaybackFile) + withContext(Dispatchers.Main) { + playbackModel.start() + } + stubPlaybackFiles.toMediaItemsWithStartPosition(stubPlaybackFile.id) + } + } + } + + @UnstableApi + private fun List.toMediaItemsWithStartPosition(currentFileId: String) = MediaItemsWithStartPosition( + map { mediaItemFactory.create(it) }, + indexOfFirst { it.id == currentFileId }, + 0, + ) + + /** + * Workaround to avoid internal media3 crash + */ + private fun getStubPlaybackFile() = PlaybackFile( + id = "0", + uri = getPlaybackUri(0L).toString(), + name = "", + mimeType = "audio/mpeg", + contentLength = 0L, + lastModified = 0L, + isFavorite = false, + ) +} diff --git a/app/src/main/java/com/ionos/player/media3/session/MediaSessionFactory.kt b/app/src/main/java/com/ionos/player/media3/session/MediaSessionFactory.kt new file mode 100644 index 000000000000..cf30c7f2976a --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/session/MediaSessionFactory.kt @@ -0,0 +1,58 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.session + +import android.content.Context +import android.os.Bundle +import androidx.media3.common.util.BitmapLoader +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.CommandButton +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionCommand +import com.ionos.player.media3.common.PlayerFactory +import com.ionos.player.media3.resumption.PlaybackResumptionPlayerListener +import com.ionos.player.model.ThumbnailLoader +import com.owncloud.android.R +import javax.inject.Inject + +class MediaSessionFactory @Inject constructor( + private val context: Context, + private val playerFactory: PlayerFactory, + private val sessionCallback: MediaSessionCallback, + private val resumptionPlayerListener: PlaybackResumptionPlayerListener, + private val thumbnailLoader: ThumbnailLoader, +) { + + @UnstableApi + fun create(): MediaSession { + val player = playerFactory.create() + player.addListener(resumptionPlayerListener) + return MediaSession + .Builder(context, player) + .setBitmapLoader(provideBitmapLoader()) + .setCallback(sessionCallback) + .setCustomLayout(provideCustomLayout()) + .build() + } + + private fun provideCustomLayout(): List { + return listOf( + CommandButton + .Builder() + .setDisplayName(context.getString(R.string.player_media_controls_close_action_title)) + .setIconResId(R.drawable.player_ic_close) + .setSessionCommand(SessionCommand(MediaSessionCallback.CLOSE_ACTION, Bundle.EMPTY)) + .build(), + ) + } + + @UnstableApi + private fun provideBitmapLoader(): BitmapLoader { + return MediaSessionBitmapLoader(context, thumbnailLoader) + } +} diff --git a/app/src/main/java/com/ionos/player/media3/session/MediaSessionHolder.kt b/app/src/main/java/com/ionos/player/media3/session/MediaSessionHolder.kt new file mode 100644 index 000000000000..22149c1af3ec --- /dev/null +++ b/app/src/main/java/com/ionos/player/media3/session/MediaSessionHolder.kt @@ -0,0 +1,17 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.media3.session + +import androidx.media3.session.MediaSession + +interface MediaSessionHolder { + + fun getMediaSession(): MediaSession + + fun release() +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFile.kt b/app/src/main/java/com/ionos/player/model/PlaybackFile.kt new file mode 100644 index 000000000000..f9e04d32601c --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFile.kt @@ -0,0 +1,24 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import java.io.Serializable + +data class PlaybackFile( + val id: String, + val uri: String, + val name: String, + val mimeType: String, + val contentLength: Long, + val lastModified: Long, + val isFavorite: Boolean, +) : Serializable { + fun getNameWithoutExtension(): String { + return name.substringBeforeLast(".") + } +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFileMapper.kt b/app/src/main/java/com/ionos/player/model/PlaybackFileMapper.kt new file mode 100644 index 000000000000..594e75dca42b --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFileMapper.kt @@ -0,0 +1,40 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.utils.MimeTypeUtil +import java.io.File + +fun OCFile.toPlaybackFile() = PlaybackFile( + id = localId.toString(), + uri = getPlaybackUri().toString(), + name = fileName, + mimeType = mimeType, + contentLength = fileLength, + lastModified = modificationTimestamp, + isFavorite = isFavorite, +) + +fun OCShare.toPlaybackFile() = PlaybackFile( + id = fileSource.toString(), + uri = getPlaybackUri().toString(), + name = path?.let { File(it).name } ?: "", + mimeType = getMimeType(), + contentLength = -1L, + lastModified = sharedDate * 1000L, + isFavorite = isFavorite, +) + +private fun OCShare.getMimeType(): String { + return mimetype + ?.takeIf { it.isNotEmpty() } + ?: path?.let { MimeTypeUtil.getMimeTypeFromPath(it) } + ?: "" +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFileType.kt b/app/src/main/java/com/ionos/player/model/PlaybackFileType.kt new file mode 100644 index 000000000000..df5b2776adfa --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFileType.kt @@ -0,0 +1,13 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +enum class PlaybackFileType(val value: String) { + AUDIO("audio"), + VIDEO("video"), +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFileUriMapper.kt b/app/src/main/java/com/ionos/player/model/PlaybackFileUriMapper.kt new file mode 100644 index 000000000000..20310561ceee --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFileUriMapper.kt @@ -0,0 +1,36 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.net.Uri +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare + +const val REMOTE_FILE_SCHEME = "remoteFile" + +fun OCFile.getPlaybackUri(): Uri { + return getPlaybackUri(localId) +} + +fun OCShare.getPlaybackUri(): Uri { + return getPlaybackUri(fileSource) +} + +fun getPlaybackUri(fileId: Long): Uri { + return Uri.Builder() + .scheme(REMOTE_FILE_SCHEME) + .authority("") + .appendPath(fileId.toString()) + .build() +} + +fun Uri.getRemoteFileId(): Long? { + return scheme + ?.takeIf { it == REMOTE_FILE_SCHEME } + ?.let { pathSegments.firstOrNull()?.toLongOrNull() } +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFiles.kt b/app/src/main/java/com/ionos/player/model/PlaybackFiles.kt new file mode 100644 index 000000000000..edf2f9e98fd0 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFiles.kt @@ -0,0 +1,13 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +data class PlaybackFiles( + val list: List, + val comparator: PlaybackFilesComparator +) diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFilesComparator.kt b/app/src/main/java/com/ionos/player/model/PlaybackFilesComparator.kt new file mode 100644 index 000000000000..009cc7a1fa57 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFilesComparator.kt @@ -0,0 +1,63 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import com.owncloud.android.utils.FileSortOrder +import third_parties.daveKoeller.AlphanumComparator + +sealed interface PlaybackFilesComparator : Comparator { + + object NONE : PlaybackFilesComparator { + override fun compare(a: PlaybackFile, b: PlaybackFile): Int { + return 0 + } + } + + object FAVORITE : PlaybackFilesComparator { + override fun compare(a: PlaybackFile, b: PlaybackFile): Int { + return AlphanumComparator.compare(a.name, b.name) + } + } + + object GALLERY : PlaybackFilesComparator { + override fun compare(a: PlaybackFile, b: PlaybackFile): Int { + return compareValuesBy(b, a) { it.lastModified } + } + } + + object SHARED : PlaybackFilesComparator { + override fun compare(a: PlaybackFile, b: PlaybackFile): Int { + return compareValuesBy(b, a) { it.lastModified } + } + } + + data class Folder( + val sortType: FileSortOrder.SortType, + val isAscending: Boolean, + ) : PlaybackFilesComparator { + private val delegate = createDelegate() + + override fun compare(a: PlaybackFile, b: PlaybackFile): Int { + return delegate.compare(a, b) + } + + private fun createDelegate(): Comparator { + val sortTypeComparator: Comparator = when (sortType) { + FileSortOrder.SortType.ALPHABET -> Comparator { a, b -> AlphanumComparator.compare(a.name, b.name) } + FileSortOrder.SortType.SIZE -> compareBy { it.contentLength } + FileSortOrder.SortType.DATE -> compareBy { it.lastModified } + } + return compareByDescending(PlaybackFile::isFavorite) + .thenComparing(if (isAscending) sortTypeComparator else sortTypeComparator.reversed()) + } + } +} + +fun FileSortOrder.toPlaybackFilesComparator(): PlaybackFilesComparator { + return PlaybackFilesComparator.Folder(getType(), isAscending) +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackFilesRepository.kt b/app/src/main/java/com/ionos/player/model/PlaybackFilesRepository.kt new file mode 100644 index 000000000000..4cbfcfc5c034 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackFilesRepository.kt @@ -0,0 +1,162 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import com.ionos.player.util.observeContentChanges +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta +import com.owncloud.android.ui.fragment.SearchType +import com.owncloud.android.utils.FileSortOrder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class PlaybackFilesRepository @Inject constructor( + private val context: Context, + private val storageManager: FileDataStorageManager, + private val preferences: AppPreferences, +) { + companion object { + private const val FETCH_DATA_DEBOUNCE_MS = 250L + } + + fun observe(folderId: Long, fileType: PlaybackFileType, searchType: SearchType?): Flow { + return when (searchType) { + SearchType.FAVORITE_SEARCH -> observeFavoritePlaybackFiles(fileType) + SearchType.GALLERY_SEARCH -> observeGalleryPlaybackFiles(fileType) + SearchType.SHARED_FILTER -> observeSharedPlaybackFiles(fileType) + else -> observeFolderPlaybackFiles(folderId, fileType, MainApp.isOnlyOnDevice()) + } + } + + suspend fun get(folderId: Long, fileType: PlaybackFileType, searchType: SearchType?): PlaybackFiles { + return when (searchType) { + SearchType.FAVORITE_SEARCH -> getFavoritePlaybackFiles(fileType) + SearchType.GALLERY_SEARCH -> getGalleryPlaybackFiles(fileType) + SearchType.SHARED_FILTER -> getSharedPlaybackFiles(fileType) + else -> getFolderPlaybackFiles(folderId, fileType, MainApp.isOnlyOnDevice()) + } + } + + private fun observeFavoritePlaybackFiles(fileType: PlaybackFileType): Flow { + val uri = ProviderTableMeta.CONTENT_URI + return observeData(uri, true) { + getFavoritePlaybackFiles(fileType) + } + } + + private suspend fun getFavoritePlaybackFiles(fileType: PlaybackFileType): PlaybackFiles { + return withContext(Dispatchers.IO) { + storageManager.favoriteFiles + .asSequence() + .filter { it.mimeType.startsWith(fileType.value, ignoreCase = true) } + .map { it.toPlaybackFile() } + .sortedWith(PlaybackFilesComparator.FAVORITE) + .let { PlaybackFiles(it.toList(), PlaybackFilesComparator.FAVORITE) } + } + } + + private fun observeGalleryPlaybackFiles(fileType: PlaybackFileType): Flow { + val uri = ProviderTableMeta.CONTENT_URI + return observeData(uri, true) { + getGalleryPlaybackFiles(fileType) + } + } + + private suspend fun getGalleryPlaybackFiles(fileType: PlaybackFileType): PlaybackFiles { + return withContext(Dispatchers.IO) { + storageManager.allGalleryItems + .asSequence() + .filter { it.mimeType.startsWith(fileType.value, ignoreCase = true) } + .map { it.toPlaybackFile() } + .sortedWith(PlaybackFilesComparator.GALLERY) + .let { PlaybackFiles(it.toList(), PlaybackFilesComparator.GALLERY) } + } + } + + private fun observeSharedPlaybackFiles(fileType: PlaybackFileType): Flow { + val uri = ProviderTableMeta.CONTENT_URI_SHARE + return observeData(uri, false) { + getSharedPlaybackFiles(fileType) + } + } + + private suspend fun getSharedPlaybackFiles(fileType: PlaybackFileType): PlaybackFiles { + return withContext(Dispatchers.IO) { + storageManager.shares + .asSequence() + .distinctBy { it.fileSource } + .map { it.toPlaybackFile() } + .filter { it.mimeType.startsWith(fileType.value, ignoreCase = true) } + .sortedWith(PlaybackFilesComparator.SHARED) + .let { PlaybackFiles(it.toList(), PlaybackFilesComparator.SHARED) } + } + } + + private fun observeFolderPlaybackFiles( + folderId: Long, + fileType: PlaybackFileType, + onDeviceOnly: Boolean, + ): Flow { + val uri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, folderId) + val sortOrderFlow = flow { + emit(getFolderSortOrder(folderId)) + } + return sortOrderFlow.flatMapConcat { sortOrder -> + val comparator = sortOrder.toPlaybackFilesComparator() + observeData(uri, false) { + getFolderPlaybackFiles(folderId, fileType, onDeviceOnly, comparator) + } + } + } + + private suspend fun getFolderPlaybackFiles( + folderId: Long, + fileType: PlaybackFileType, + onDeviceOnly: Boolean, + comparator: PlaybackFilesComparator? = null, + ): PlaybackFiles { + return withContext(Dispatchers.IO) { + val folder = storageManager.getFileById(folderId) ?: throw IllegalStateException("Folder not found") + val comparator = comparator ?: preferences.getSortOrderByFolder(folder).toPlaybackFilesComparator() + storageManager.getFolderContent(folder, onDeviceOnly) + .asSequence() + .filter { it.mimeType.startsWith(fileType.value, ignoreCase = true) } + .map { it.toPlaybackFile() } + .sortedWith(comparator) + .let { PlaybackFiles(it.toList(), comparator) } + } + } + + private suspend fun getFolderSortOrder(folderId: Long): FileSortOrder { + return withContext(Dispatchers.IO) { + val folder = storageManager.getFileById(folderId) ?: throw IllegalStateException("Folder not found") + preferences.getSortOrderByFolder(folder) + } + } + + private fun observeData(uri: Uri, notifyForDescendants: Boolean, fetchData: suspend () -> T): Flow { + return context.contentResolver.observeContentChanges(uri, notifyForDescendants) + .debounce(FETCH_DATA_DEBOUNCE_MS) // Debounce to avoid too frequent data fetching for batch updates + .map { fetchData() } + .onStart { emit(fetchData()) } + .distinctUntilChanged() + } +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackModel.kt b/app/src/main/java/com/ionos/player/model/PlaybackModel.kt new file mode 100644 index 000000000000..51e5a259da9d --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackModel.kt @@ -0,0 +1,58 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.view.SurfaceView +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.RepeatMode +import kotlinx.coroutines.flow.Flow +import java.util.Optional + +interface PlaybackModel { + + val state: Optional + + suspend fun start() + + fun setFilesFlow(filesFlow: Flow) + + fun setFiles(files: PlaybackFiles) + + fun release() + + fun setVideoSurfaceView(surfaceView: SurfaceView?) + + fun addListener(listener: Listener) + + fun removeListener(listener: Listener) + + fun play() + + fun pause() + + fun stop() + + fun playNext() + + fun playPrevious() + + fun seekToPosition(positionInMilliseconds: Int) + + fun setRepeatMode(repeatMode: RepeatMode) + + fun setShuffle(shuffle: Boolean) + + fun switchToFile(file: PlaybackFile) + + interface Listener { + + fun onPlaybackUpdate(state: PlaybackState) + + fun onPlaybackError(error: Throwable) + } +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackModelCompositeListener.kt b/app/src/main/java/com/ionos/player/model/PlaybackModelCompositeListener.kt new file mode 100644 index 000000000000..0c67ffb87f93 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackModelCompositeListener.kt @@ -0,0 +1,36 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import com.ionos.player.model.state.PlaybackState + +class PlaybackModelCompositeListener : PlaybackModel.Listener { + private val listeners = mutableListOf() + + fun addListener(listener: PlaybackModel.Listener) { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + + fun removeListener(listener: PlaybackModel.Listener?) { + listeners.remove(listener) + } + + override fun onPlaybackUpdate(state: PlaybackState) { + for (i in 0 until listeners.size) { + listeners.getOrNull(i)?.onPlaybackUpdate(state) + } + } + + override fun onPlaybackError(error: Throwable) { + for (i in 0 until listeners.size) { + listeners.getOrNull(i)?.onPlaybackError(error) + } + } +} diff --git a/app/src/main/java/com/ionos/player/model/PlaybackSettings.kt b/app/src/main/java/com/ionos/player/model/PlaybackSettings.kt new file mode 100644 index 000000000000..55c80fb637e6 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/PlaybackSettings.kt @@ -0,0 +1,46 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.content.Context +import androidx.core.content.edit +import com.ionos.player.model.state.RepeatMode +import javax.inject.Inject + +class PlaybackSettings @Inject constructor( + context: Context, +) { + companion object { + private const val PREFERENCES_FILE_NAME = "playback_settings" + private const val REPEAT_MODE_ID_KEY = "repeat_mode_id" + private const val SHUFFLE_KEY = "shuffle" + private val DEFAULT_REPEAT_MODE = RepeatMode.ALL + } + + private val preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) + + val repeatMode: RepeatMode + get() = preferences.getInt(REPEAT_MODE_ID_KEY, -1) + .let { id -> RepeatMode.entries.firstOrNull { it.id == id } } + ?: DEFAULT_REPEAT_MODE + + val isShuffle: Boolean + get() = preferences.getBoolean(SHUFFLE_KEY, false) + + fun setRepeatMode(repeatMode: RepeatMode) { + preferences.edit { + putInt(REPEAT_MODE_ID_KEY, repeatMode.id) + } + } + + fun setShuffle(shuffle: Boolean) { + preferences.edit { + putBoolean(SHUFFLE_KEY, shuffle) + } + } +} diff --git a/app/src/main/java/com/ionos/player/model/SourceException.kt b/app/src/main/java/com/ionos/player/model/SourceException.kt new file mode 100644 index 000000000000..7714dd452933 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/SourceException.kt @@ -0,0 +1,14 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +class SourceException( + errorCode: Int = 0, +) : Exception( + "Source not found. Error code: $errorCode", +) diff --git a/app/src/main/java/com/ionos/player/model/ThumbnailLoader.kt b/app/src/main/java/com/ionos/player/model/ThumbnailLoader.kt new file mode 100644 index 000000000000..f1156a7ebf62 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/ThumbnailLoader.kt @@ -0,0 +1,96 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.content.Context +import android.graphics.Bitmap +import android.widget.ImageView +import androidx.core.graphics.drawable.toBitmap +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.utils.GlideHelper +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class ThumbnailLoader @Inject constructor( + private val userAccountManager: UserAccountManager, + private val clientFactory: ClientFactory, +) { + private val user by lazy { userAccountManager.user } + private val getThumbnailUrl by lazy { + clientFactory.create(user).baseUri.toString() + "/index.php/core/preview" + } + + suspend fun await(context: Context, file: PlaybackFile, width: Int, height: Int): Bitmap? { + return withContext(Dispatchers.IO) { + try { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + + val url = "$getThumbnailUrl?fileId=${file.id}&x=$width&y=$height&a=1&mode=cover&forceIcon=0" + + val drawable = GlideHelper.getDrawable(context, client, url) + drawable?.toBitmap(width, height) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + null + } + } + } + + suspend fun load(context: Context, file: PlaybackFile, width: Int, height: Int): Bitmap? { + return await(context, file, width, height) + } + + suspend fun load(context: Context, model: Any, fileId: String?, width: Int, height: Int): Bitmap? { + return withContext(Dispatchers.IO) { + try { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + + val url = when (model) { + is String -> model + else -> model.toString() + } + + val drawable = GlideHelper.getDrawable(context, client, url) + drawable?.toBitmap(width, height) + } catch (e: Exception) { + null + } + } + } + + suspend fun load(imageView: ImageView, model: Any, fileId: String) { + withContext(Dispatchers.IO) { + try { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), imageView.context) + + val url = when (model) { + is String -> model + else -> model.toString() + } + + val drawable = GlideHelper.getDrawable(imageView.context, client, url) + + withContext(Dispatchers.Main) { + drawable?.let { + imageView.setImageDrawable(it) + } + } + } catch (e: Exception) { + // Handle error if needed + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/model/VideoViewSetter.kt b/app/src/main/java/com/ionos/player/model/VideoViewSetter.kt new file mode 100644 index 000000000000..267f4ab2f583 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/VideoViewSetter.kt @@ -0,0 +1,14 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model + +import android.view.SurfaceHolder + +fun interface VideoViewSetter { + fun setVideoView(holder: SurfaceHolder?) +} diff --git a/app/src/main/java/com/ionos/player/model/error_strategy/DefaultPlaybackErrorStrategy.kt b/app/src/main/java/com/ionos/player/model/error_strategy/DefaultPlaybackErrorStrategy.kt new file mode 100644 index 000000000000..7d3383d4ec14 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/error_strategy/DefaultPlaybackErrorStrategy.kt @@ -0,0 +1,22 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.error_strategy + +import com.ionos.player.model.state.PlaybackState +import javax.inject.Inject + +class DefaultPlaybackErrorStrategy @Inject constructor() : PlaybackErrorStrategy { + + override fun switchToNextSource(throwable: Throwable, playbackState: PlaybackState): Boolean { + val currentFile = playbackState.currentItemState?.file + val currentFiles = playbackState.currentFiles + val oneFileInQueue = currentFiles.size == 1 + val endOfQueue = currentFiles.indexOf(currentFile) == currentFiles.lastIndex + return !oneFileInQueue && !endOfQueue + } +} diff --git a/app/src/main/java/com/ionos/player/model/error_strategy/PlaybackErrorStrategy.kt b/app/src/main/java/com/ionos/player/model/error_strategy/PlaybackErrorStrategy.kt new file mode 100644 index 000000000000..9a051e6db5ef --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/error_strategy/PlaybackErrorStrategy.kt @@ -0,0 +1,15 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.error_strategy + +import com.ionos.player.model.state.PlaybackState +import java.io.Serializable + +interface PlaybackErrorStrategy : Serializable { + fun switchToNextSource(error: Throwable, state: PlaybackState): Boolean +} diff --git a/app/src/main/java/com/ionos/player/model/state/PlaybackItemMetadata.kt b/app/src/main/java/com/ionos/player/model/state/PlaybackItemMetadata.kt new file mode 100644 index 000000000000..ff2f57889ee8 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/PlaybackItemMetadata.kt @@ -0,0 +1,19 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +data class PlaybackItemMetadata @JvmOverloads constructor( + val title: CharSequence, + val artist: CharSequence? = null, + val album: CharSequence? = null, + val genre: CharSequence? = null, + val year: Int? = null, + val description: CharSequence? = null, + val artworkData: ByteArray? = null, + val artworkUri: CharSequence? = null, +) diff --git a/app/src/main/java/com/ionos/player/model/state/PlaybackItemState.kt b/app/src/main/java/com/ionos/player/model/state/PlaybackItemState.kt new file mode 100644 index 000000000000..003a28f4ffa3 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/PlaybackItemState.kt @@ -0,0 +1,20 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +import com.ionos.player.model.PlaybackFile +import java.io.Serializable + +data class PlaybackItemState( + val file: PlaybackFile, + val playerState: PlayerState, + val metadata: PlaybackItemMetadata?, + val videoSize: VideoSize?, + val currentTimeInMilliseconds: Int, + val maxTimeInMilliseconds: Int, +) : Serializable diff --git a/app/src/main/java/com/ionos/player/model/state/PlaybackState.kt b/app/src/main/java/com/ionos/player/model/state/PlaybackState.kt new file mode 100644 index 000000000000..39ee4d2a6371 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/PlaybackState.kt @@ -0,0 +1,18 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +import com.ionos.player.model.PlaybackFile +import java.io.Serializable + +class PlaybackState( + val currentFiles: List, + val currentItemState: PlaybackItemState?, + val repeatMode: RepeatMode, + val shuffle: Boolean, +) : Serializable diff --git a/app/src/main/java/com/ionos/player/model/state/PlayerState.kt b/app/src/main/java/com/ionos/player/model/state/PlayerState.kt new file mode 100644 index 000000000000..901b048cbb03 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/PlayerState.kt @@ -0,0 +1,18 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +import java.io.Serializable + +enum class PlayerState : Serializable { + IDLE, + PLAYING, + PAUSED, + COMPLETED, + NONE, +} diff --git a/app/src/main/java/com/ionos/player/model/state/PlayerStateAnalyzer.kt b/app/src/main/java/com/ionos/player/model/state/PlayerStateAnalyzer.kt new file mode 100644 index 000000000000..e1d037894b16 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/PlayerStateAnalyzer.kt @@ -0,0 +1,30 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +class PlayerStateAnalyzer( + private val state: PlayerState, +) { + companion object { + private val ALLOWED_PLAY_STATES = setOf(PlayerState.IDLE, PlayerState.PAUSED, PlayerState.COMPLETED) + private val ALLOWED_PAUSE_STATES = setOf(PlayerState.PLAYING) + private val ALLOWED_STOP_STATES = setOf(PlayerState.PLAYING, PlayerState.PAUSED, PlayerState.COMPLETED) + } + + fun playAvailable(): Boolean { + return ALLOWED_PLAY_STATES.contains(state) + } + + fun pauseAvailable(): Boolean { + return ALLOWED_PAUSE_STATES.contains(state) + } + + fun stopAvailable(): Boolean { + return ALLOWED_STOP_STATES.contains(state) + } +} diff --git a/app/src/main/java/com/ionos/player/model/state/RepeatMode.kt b/app/src/main/java/com/ionos/player/model/state/RepeatMode.kt new file mode 100644 index 000000000000..cfe975a0eec7 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/RepeatMode.kt @@ -0,0 +1,16 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +import java.io.Serializable + +enum class RepeatMode(val id: Int) : Serializable { + OFF(0), + SINGLE(1), + ALL(2), +} diff --git a/app/src/main/java/com/ionos/player/model/state/VideoSize.kt b/app/src/main/java/com/ionos/player/model/state/VideoSize.kt new file mode 100644 index 000000000000..0d90d9deeaf2 --- /dev/null +++ b/app/src/main/java/com/ionos/player/model/state/VideoSize.kt @@ -0,0 +1,15 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.model.state + +import java.io.Serializable + +data class VideoSize( + val width: Int, + val height: Int, +) : Serializable diff --git a/app/src/main/java/com/ionos/player/ui/PlayerActivity.kt b/app/src/main/java/com/ionos/player/ui/PlayerActivity.kt new file mode 100644 index 000000000000..8683be1eda0c --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/PlayerActivity.kt @@ -0,0 +1,253 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui + +import android.app.PictureInPictureParams +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Rect +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.util.Rational +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.annotation.RequiresApi +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.ionos.annotation.IonosCustomization +import com.ionos.player.model.PlaybackFileType +import com.ionos.player.model.PlaybackModel +import com.ionos.player.ui.PlayerScreenEvent.LaunchOpenFileIntent +import com.ionos.player.ui.PlayerScreenEvent.LaunchStreamFileIntent +import com.ionos.player.ui.PlayerScreenEvent.ShowFileActions +import com.ionos.player.ui.PlayerScreenEvent.ShowFileDetails +import com.ionos.player.ui.PlayerScreenEvent.ShowFileExportStartedMessage +import com.ionos.player.ui.PlayerScreenEvent.ShowRemoveFileDialog +import com.ionos.player.ui.PlayerScreenEvent.ShowShareFileDialog +import com.ionos.player.ui.audio.AudioPlayerView +import com.ionos.player.ui.video.VideoPlayerView +import com.ionos.player.util.SystemVersion +import com.ionos.player.util.isPictureInPictureAllowed +import com.nextcloud.client.di.Injectable +import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionsBottomSheet +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment +import com.owncloud.android.utils.DisplayUtils +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class PlayerActivity : FileActivity(), Injectable { + + companion object { + private const val PLAYBACK_FILE_TYPE: String = "PLAYBACK_FILE_TYPE" + + fun createIntent(context: Context, playbackFileType: PlaybackFileType): Intent { + return Intent(context, PlayerActivity::class.java).apply { + putExtra(PLAYBACK_FILE_TYPE, playbackFileType) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + } + } + } + + @Inject + lateinit var playbackModel: PlaybackModel + + @Inject + lateinit var viewModelFactory: PlayerViewModel.Factory + + private val viewModel by viewModels { viewModelFactory } + + private lateinit var playbackFileType: PlaybackFileType + + private lateinit var playerView: PlayerView + + private val pipAspectRatio = Rational(16, 9) + + private var onBackPressedCallback: OnBackPressedCallback? = null + + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + playbackFileType = intent.getPlaybackFileType() + createPlayerView() + + viewModel.eventFlow + .flowWithLifecycle(lifecycle) + .onEach { handleEvent(it) } + .launchIn(lifecycleScope) + + if (isPictureInPictureAllowed()) { + val isVideoPlayback = playbackFileType == PlaybackFileType.VIDEO + onBackPressedCallback = onBackPressedDispatcher.addCallback(this, enabled = isVideoPlayback) { + switchToPictureInPictureMode() + } + } + + volumeControlStream = AudioManager.STREAM_MUSIC + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + playbackFileType = intent.getPlaybackFileType() + recreatePlayerView() + onBackPressedCallback?.isEnabled = canUsePictureInPictureMode() + } + + private fun createPlayerView() { + playerView = when (playbackFileType) { + PlaybackFileType.AUDIO -> AudioPlayerView(this) + PlaybackFileType.VIDEO -> VideoPlayerView(this) + } + val moreButton = playerView.findViewById(R.id.more) + moreButton.setOnClickListener { viewModel.onMoreButtonClick() } + setContentView(playerView) + } + + private fun recreatePlayerView() { + playerView.onStop() + createPlayerView() + playerView.onStart() + } + + @Suppress("DEPRECATION") + private fun Intent.getPlaybackFileType(): PlaybackFileType { + val playbackFileType = if (SystemVersion.greaterOrEqualToTiramisu()) { + getSerializableExtra(PLAYBACK_FILE_TYPE, PlaybackFileType::class.java) + } else { + getSerializableExtra(PLAYBACK_FILE_TYPE) as PlaybackFileType? + } + return playbackFileType ?: throw IllegalStateException("Playback file type was not defined") + } + + override fun onStart() { + super.onStart() + playerView.onStart() + } + + override fun onStop() { + super.onStop() + playerView.onStop() + } + + override fun onDestroy() { + super.onDestroy() + if (isFinishing && playbackFileType == PlaybackFileType.VIDEO) { + playbackModel.release() + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + recreatePlayerView() + if (isInPictureInPictureMode) { + (playerView as? VideoPlayerView)?.hideControls() + } else { + (playerView as? VideoPlayerView)?.showControls() + } + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (canUsePictureInPictureMode()) { + switchToPictureInPictureMode() + } + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + if (!isInPictureInPictureMode && lifecycle.currentState == Lifecycle.State.CREATED) { + finish() // Finish the activity if the user closes the PIP window + } + } + + private fun canUsePictureInPictureMode(): Boolean { + return playbackFileType == PlaybackFileType.VIDEO && isPictureInPictureAllowed() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun switchToPictureInPictureMode() { + val params = createPictureInPictureParams() + enterPictureInPictureMode(params) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createPictureInPictureParams(): PictureInPictureParams { + return PictureInPictureParams.Builder().let { + it.setAspectRatio(pipAspectRatio) + getSourceRectHint().let(it::setSourceRectHint) + it.build() + } + } + + private fun getSourceRectHint(): Rect? { + val containerRect = Rect() + playerView.getGlobalVisibleRect(containerRect) + val sourceHeightHint = (containerRect.width() / pipAspectRatio.toFloat()).toInt() + return Rect( + containerRect.left, + containerRect.top + (containerRect.height() - sourceHeightHint) / 2, + containerRect.right, + containerRect.top + (containerRect.height() + sourceHeightHint) / 2, + ) + } + + private fun handleEvent(event: PlayerScreenEvent) { + when (event) { + is ShowFileActions -> showFileActions(event.file, event.actionIds) + is ShowFileDetails -> showFileDetails(event.file) + is ShowFileExportStartedMessage -> showFileExportStartedMessage() + is ShowShareFileDialog -> fileOperationsHelper.sendShareFile(event.file) + is ShowRemoveFileDialog -> showRemoveFileDialog(event.file) + is LaunchOpenFileIntent -> fileOperationsHelper.openFile(event.file) + is LaunchStreamFileIntent -> fileOperationsHelper.streamMediaFile(event.file) + } + } + + private fun showFileActions(file: OCFile, actionIds: List) { + val actionsToHide = FileAction.getActions(listOf(file)).map(FileAction::id).filter { it !in actionIds } + FileActionsBottomSheet.newInstance(file, false, actionsToHide) + .setResultListener(supportFragmentManager, this) { viewModel.onFileActionChosen(file, it) } + .show(supportFragmentManager, "actions") + } + + private fun showFileDetails(file: OCFile) { + val intent = Intent(this, FileDisplayActivity::class.java).apply { + action = FileDisplayActivity.ACTION_DETAILS + putExtra(EXTRA_FILE, file) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + startActivity(intent) + finish() + } + + private fun showFileExportStartedMessage() { + val message = resources.getQuantityString(R.plurals.export_start, 1, 1) + DisplayUtils.showSnackMessage(playerView, message) + } + + private fun showRemoveFileDialog(file: OCFile) { + RemoveFilesDialogFragment.newInstance(file) + .show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + } +} diff --git a/app/src/main/java/com/ionos/player/ui/PlayerLauncher.kt b/app/src/main/java/com/ionos/player/ui/PlayerLauncher.kt new file mode 100644 index 000000000000..bc1434ed6934 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/PlayerLauncher.kt @@ -0,0 +1,63 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.ionos.player.media3.resumption.PlaybackResumptionConfigStore +import com.ionos.player.model.PlaybackFileType +import com.ionos.player.model.PlaybackFiles +import com.ionos.player.model.PlaybackFilesComparator +import com.ionos.player.model.PlaybackFilesRepository +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.toPlaybackFile +import com.nextcloud.client.logger.Logger +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.fragment.SearchType +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.util.concurrent.CancellationException +import javax.inject.Inject + +class PlayerLauncher @Inject constructor( + private val playbackResumptionConfigStore: PlaybackResumptionConfigStore, + private val playbackFilesRepository: PlaybackFilesRepository, + private val playbackModel: PlaybackModel, + private val logger: Logger, +) { + private var currentLaunchJob: Job? = null + + fun launch(activity: AppCompatActivity, file: OCFile, searchType: SearchType?) { + currentLaunchJob?.cancel() + currentLaunchJob = activity.lifecycleScope.launch { + try { + val fileType = file.getPlaybackFileType() + playbackResumptionConfigStore.saveConfig(file.localId.toString(), file.parentId, fileType, searchType) + + val currentPlaybackFile = file.toPlaybackFile() + + playbackModel.start() + playbackModel.setFiles(PlaybackFiles(listOf(currentPlaybackFile), PlaybackFilesComparator.NONE)) + playbackModel.setFilesFlow(playbackFilesRepository.observe(file.parentId, fileType, searchType)) + playbackModel.play() + + val intent = PlayerActivity.createIntent(activity, fileType) + activity.startActivity(intent) + } catch (t: Throwable) { + if (t is CancellationException) throw t + logger.e(PlayerLauncher::class.java.simpleName, "Error launching player", t) + } + } + } + + private fun OCFile.getPlaybackFileType(): PlaybackFileType { + return PlaybackFileType.entries + .firstOrNull { mimeType.startsWith(it.value, ignoreCase = true) } + ?: throw IllegalArgumentException("Unsupported file type: $mimeType") + } +} diff --git a/app/src/main/java/com/ionos/player/ui/PlayerScreenEvent.kt b/app/src/main/java/com/ionos/player/ui/PlayerScreenEvent.kt new file mode 100644 index 000000000000..0ab3ebcc4bdc --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/PlayerScreenEvent.kt @@ -0,0 +1,27 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui + +import com.owncloud.android.datamodel.OCFile + +sealed interface PlayerScreenEvent { + + data class ShowFileActions(val file: OCFile, val actionIds: List) : PlayerScreenEvent + + data class ShowFileDetails(val file: OCFile) : PlayerScreenEvent + + data object ShowFileExportStartedMessage : PlayerScreenEvent + + data class ShowShareFileDialog(val file: OCFile) : PlayerScreenEvent + + data class ShowRemoveFileDialog(val file: OCFile) : PlayerScreenEvent + + data class LaunchOpenFileIntent(val file: OCFile) : PlayerScreenEvent + + data class LaunchStreamFileIntent(val file: OCFile) : PlayerScreenEvent +} diff --git a/app/src/main/java/com/ionos/player/ui/PlayerView.kt b/app/src/main/java/com/ionos/player/ui/PlayerView.kt new file mode 100644 index 000000000000..41bec61fc747 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/PlayerView.kt @@ -0,0 +1,117 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.SourceException +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.ui.control.PlayerControlView +import com.ionos.player.ui.pager.PlayerPager +import com.ionos.player.ui.pager.PlayerPagerFragmentFactory +import com.ionos.player.ui.pager.PlayerPagerMode +import com.ionos.player.util.WindowWrapper +import com.owncloud.android.R +import com.owncloud.android.utils.DisplayUtils +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +abstract class PlayerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr), + PlaybackModel.Listener { + + @Inject + lateinit var playbackModel: PlaybackModel + + @get:LayoutRes + protected abstract val layoutRes: Int + + protected abstract val fragmentFactory: PlayerPagerFragmentFactory + + protected val activity: AppCompatActivity by lazy { context as AppCompatActivity } + protected val windowWrapper: WindowWrapper by lazy { WindowWrapper(activity.window) } + + protected val topBar: View by lazy { findViewById(R.id.topBar) } + protected val titleTextView: TextView by lazy { findViewById(R.id.title) } + protected val playerPager: PlayerPager by lazy { findViewById(R.id.playerPager) } + protected val playerControlView: PlayerControlView by lazy { findViewById(R.id.playerControlView) } + + init { + inflate(context, layoutRes, this) + if (!isInEditMode) { + inject(context) + playerPager.init(activity.supportFragmentManager, PlayerPagerMode.INFINITE, fragmentFactory) + playerPager.setPlayerPagerListener { playbackModel.switchToFile(it) } + findViewById(R.id.back).setOnClickListener { activity.onBackPressedDispatcher.onBackPressed() } + } + } + + protected abstract fun inject(context: Context) + + @CallSuper + open fun onStart() { + val state = playbackModel.state.getOrNull() + if (state == null) { + activity.finish() + return + } + + render(state) + playbackModel.addListener(this) + playerControlView.onStart() + } + + @CallSuper + open fun onStop() { + playbackModel.removeListener(this) + playerControlView.onStop() + } + + override fun onPlaybackUpdate(state: PlaybackState) { + render(state) + } + + override fun onPlaybackError(error: Throwable) { + if (error is SourceException) { + DisplayUtils.showSnackMessage(this, R.string.player_error_source_not_found) + } else { + DisplayUtils.showSnackMessage(this, R.string.player_error_unknown) + } + } + + private fun render(state: PlaybackState) { + val currentFiles = state.currentFiles + if (state.currentFiles.isEmpty()) { + activity.finish() + return + } + + if (playerPager.getItems() != currentFiles) { + playerPager.setItems(currentFiles) + } + + if (state.currentItemState != null) { + val file = state.currentItemState.file + titleTextView.text = file.getNameWithoutExtension() + playerPager.currentItem = file + } else { + titleTextView.text = "" + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/PlayerViewModel.kt b/app/src/main/java/com/ionos/player/ui/PlayerViewModel.kt new file mode 100644 index 000000000000..6ce7629d4ff3 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/PlayerViewModel.kt @@ -0,0 +1,124 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui + +import androidx.core.text.isDigitsOnly +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.ionos.player.model.PlaybackModel +import com.ionos.player.ui.PlayerScreenEvent.LaunchOpenFileIntent +import com.ionos.player.ui.PlayerScreenEvent.LaunchStreamFileIntent +import com.ionos.player.ui.PlayerScreenEvent.ShowFileActions +import com.ionos.player.ui.PlayerScreenEvent.ShowFileDetails +import com.ionos.player.ui.PlayerScreenEvent.ShowFileExportStartedMessage +import com.ionos.player.ui.PlayerScreenEvent.ShowRemoveFileDialog +import com.ionos.player.ui.PlayerScreenEvent.ShowShareFileDialog +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.logger.Logger +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.cancellation.CancellationException +import kotlin.jvm.optionals.getOrNull + +class PlayerViewModel @Inject constructor( + private val playbackModel: PlaybackModel, + private val storageManager: FileDataStorageManager, + private val userAccountManager: UserAccountManager, + private val backgroundJobManager: BackgroundJobManager, + private val logger: Logger, +) : ViewModel() { + + private val eventChannel = Channel(Channel.BUFFERED) + val eventFlow: Flow = eventChannel.receiveAsFlow() + + fun onMoreButtonClick() { + viewModelScope.launch { + val file = getCurrentOCFile() ?: return@launch + val actionIds = listOf( + R.id.action_see_details, + R.id.action_download_file, + R.id.action_export_file, + R.id.action_send_share_file, + R.id.action_remove_file, + R.id.action_open_file_with, + R.id.action_stream_media, + ) + eventChannel.trySend(ShowFileActions(file, actionIds)) + } + } + + fun onFileActionChosen(file: OCFile, actionId: Int) { + when (actionId) { + R.id.action_see_details -> eventChannel.trySend(ShowFileDetails(file)) + R.id.action_download_file -> startFileDownloading(file) + R.id.action_export_file -> startFileExport(file) + R.id.action_send_share_file -> eventChannel.trySend(ShowShareFileDialog(file)) + R.id.action_remove_file -> eventChannel.trySend(ShowRemoveFileDialog(file)) + R.id.action_open_file_with -> onOpenFileWithClick(file) + R.id.action_stream_media -> onStreamFileClick(file) + } + } + + private suspend fun getCurrentOCFile(): OCFile? { + val currentFileId = playbackModel.state.getOrNull()?.currentItemState?.file?.id + return currentFileId + ?.takeIf { it.isDigitsOnly() } + ?.let { getOCFile(it.toLong()) } + } + + private suspend fun getOCFile(localId: Long): OCFile? = withContext(Dispatchers.IO) { + try { + storageManager.getFileByLocalId(localId) + } catch (e: Exception) { + if (e is CancellationException) throw e + logger.e(PlayerViewModel::class.java.simpleName, "Failed to get file by localId: $localId", e) + null + } + } + + private fun startFileDownloading(file: OCFile) { + val user = userAccountManager.user + FileDownloadHelper.instance().downloadFileIfNotStartedBefore(user, file) + } + + private fun startFileExport(file: OCFile) { + backgroundJobManager.startImmediateFilesExportJob(listOf(file)) + eventChannel.trySend(ShowFileExportStartedMessage) + } + + private fun onOpenFileWithClick(file: OCFile) { + playbackModel.pause() + eventChannel.trySend(LaunchOpenFileIntent(file)) + } + + private fun onStreamFileClick(file: OCFile) { + playbackModel.pause() + eventChannel.trySend(LaunchStreamFileIntent(file)) + } + + class Factory @Inject constructor( + private val viewModelProvider: Provider, + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return viewModelProvider.get() as T + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragment.kt b/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragment.kt new file mode 100644 index 000000000000..010d1cb1108e --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragment.kt @@ -0,0 +1,130 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.audio + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.ThumbnailLoader +import com.ionos.player.model.state.PlaybackItemMetadata +import com.ionos.player.model.state.PlaybackState +import com.owncloud.android.R +import com.owncloud.android.databinding.PlayerAudioFileFragmentBinding +import com.owncloud.android.utils.DisplayUtils +import dagger.android.support.AndroidSupportInjection +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AudioFileFragment : Fragment(), PlaybackModel.Listener { + + companion object { + private const val ARGUMENT_FILE = "ARGUMENT_FILE" + + fun createInstance(file: PlaybackFile) = AudioFileFragment().apply { + arguments = bundleOf(ARGUMENT_FILE to file) + } + } + + @Inject + lateinit var playbackModel: PlaybackModel + + @Inject + lateinit var thumbnailLoader: ThumbnailLoader + + private lateinit var binding: PlayerAudioFileFragmentBinding + private lateinit var file: PlaybackFile + private var loadFileThumbnailJob: Job? = null + private var isFileThumbnailLoaded = false + private var metadata: PlaybackItemMetadata? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidSupportInjection.inject(this) + file = arguments?.getSerializable(ARGUMENT_FILE) as PlaybackFile + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = PlayerAudioFileFragmentBinding.inflate(inflater, container, false) + binding.title.isSelected = true + binding.title.text = file.getNameWithoutExtension() + binding.fileDetails.text = file.getDetailsText() + loadFileThumbnailJob = loadFileThumbnail() + return binding.root + } + + override fun onStart() { + super.onStart() + playbackModel.state.ifPresent(::onPlaybackUpdate) + playbackModel.addListener(this) + } + + override fun onStop() { + playbackModel.removeListener(this) + super.onStop() + } + + override fun onPlaybackUpdate(state: PlaybackState) { + state.currentItemState?.let { + if (it.file.id == file.id && it.metadata != null && it.metadata != metadata) { + onMetadataUpdate(it.metadata) + } + } + } + + override fun onPlaybackError(error: Throwable) { + } + + private fun onMetadataUpdate(metadata: PlaybackItemMetadata) { + this.metadata = metadata + if (!isFileThumbnailLoaded && (metadata.artworkData != null || metadata.artworkUri != null)) { + loadFileThumbnailJob?.takeIf { it.isActive }?.cancel() + loadMetadataArtwork(metadata) + } + binding.title.text = if (metadata.artist.isNullOrEmpty()) { + metadata.title + } else { + getString(R.string.player_audio_source_artist_and_title, metadata.artist, metadata.title) + } + } + + private fun loadFileThumbnail(): Job { + return viewLifecycleOwner.lifecycleScope.launch { + val thumbnailSize = resources.getDimension(R.dimen.player_album_cover_size).toInt() + val thumbnail = thumbnailLoader.await(requireContext(), file, thumbnailSize, thumbnailSize) + if (thumbnail != null) { + binding.albumCover.setImageBitmap(thumbnail) + isFileThumbnailLoaded = true + } + } + } + + private fun loadMetadataArtwork(metadata: PlaybackItemMetadata) { + val source = metadata.artworkData ?: metadata.artworkUri ?: return + viewLifecycleOwner.lifecycleScope.launch { + thumbnailLoader.load(binding.albumCover, source, file.id) + } + } + + private fun PlaybackFile.getDetailsText(): String { + val size = if (contentLength > 0) DisplayUtils.bytesToHumanReadable(contentLength) else "" + val date = if (lastModified > 0) getLastModifiedText(lastModified) else "" + return if (size.isNotEmpty() && date.isNotEmpty()) "$size, $date" else size + date + } + + private fun getLastModifiedText(lastModified: Long): String { + val relativeTimestamp = DisplayUtils.getRelativeTimestamp(context, lastModified) + return getString(R.string.player_last_modified, relativeTimestamp) + } +} diff --git a/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragmentFactory.kt b/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragmentFactory.kt new file mode 100644 index 000000000000..4d33fcea2084 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/audio/AudioFileFragmentFactory.kt @@ -0,0 +1,19 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.audio + +import androidx.fragment.app.Fragment +import com.ionos.player.model.PlaybackFile +import com.ionos.player.ui.pager.PlayerPagerFragmentFactory + +class AudioFileFragmentFactory : PlayerPagerFragmentFactory { + + override fun create(item: PlaybackFile): Fragment { + return AudioFileFragment.createInstance(item) + } +} diff --git a/app/src/main/java/com/ionos/player/ui/audio/AudioPlayerView.kt b/app/src/main/java/com/ionos/player/ui/audio/AudioPlayerView.kt new file mode 100644 index 000000000000..ba392b3e6526 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/audio/AudioPlayerView.kt @@ -0,0 +1,46 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.audio + +import android.content.Context +import android.view.WindowInsets +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import com.ionos.player.ui.PlayerView +import com.owncloud.android.R +import dagger.android.HasAndroidInjector + +class AudioPlayerView(context: Context) : PlayerView(context) { + + override val layoutRes get() = R.layout.player_audio_view + + override val fragmentFactory get() = AudioFileFragmentFactory() + + override fun inject(context: Context) { + (context.applicationContext as HasAndroidInjector).androidInjector().inject(this) + } + + override fun onStart() { + super.onStart() + windowWrapper.showSystemBars() + } + + override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { + val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowInsets) + val insets = windowInsetsCompat.getInsets(Type.systemBars() or Type.displayCutout()) + + topBar.setPadding(insets.left, insets.top, insets.right, 0) + playerPager.setPadding(insets.left, 0, insets.right, 0) + playerControlView.setPadding(insets.left, 0, insets.right, insets.bottom) + + windowWrapper.setupStatusBar(R.color.player_background_color, false) + windowWrapper.setupNavigationBar(R.color.player_background_color, true) + + return WindowInsetsCompat.CONSUMED.toWindowInsets() + } +} diff --git a/app/src/main/java/com/ionos/player/ui/common/PlayerBasePresenter.kt b/app/src/main/java/com/ionos/player/ui/common/PlayerBasePresenter.kt new file mode 100644 index 000000000000..7358ccf34ca5 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/common/PlayerBasePresenter.kt @@ -0,0 +1,19 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.common + +interface PlayerBasePresenter { + + fun onCreate() + + fun onDestroy() + + fun onAppear() + + fun onDisappear() +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/ui/common/PlayerProgressIndicator.kt b/app/src/main/java/com/ionos/player/ui/common/PlayerProgressIndicator.kt new file mode 100644 index 000000000000..984d9a56b4d6 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/common/PlayerProgressIndicator.kt @@ -0,0 +1,84 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.common + +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.AttrRes +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.state.PlaybackItemState +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.PlayerState +import com.ionos.player.model.toPlaybackFile +import com.owncloud.android.datamodel.OCFile +import dagger.android.HasAndroidInjector +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +class PlayerProgressIndicator @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : LinearProgressIndicator(context, attrs, defStyleAttr), + PlaybackModel.Listener { + + @Inject + lateinit var playbackModel: PlaybackModel + + private var playbackFile: PlaybackFile? = null + + init { + indicatorTrackGapSize = 0 + trackStopIndicatorSize = 0 + if (!isInEditMode) { + (context.applicationContext as HasAndroidInjector).androidInjector().inject(this) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + renderCurrentState() + playbackModel.addListener(this) + } + + override fun onDetachedFromWindow() { + playbackModel.removeListener(this) + visibility = GONE + super.onDetachedFromWindow() + } + + override fun onPlaybackUpdate(state: PlaybackState) { + val itemState = state.currentItemState + render(itemState) + } + + override fun onPlaybackError(error: Throwable) { + } + + fun setFile(file: OCFile) { + playbackFile = file.toPlaybackFile() + renderCurrentState() + } + + private fun renderCurrentState() { + val itemState = playbackModel.state.getOrNull()?.currentItemState + render(itemState) + } + + private fun render(itemState: PlaybackItemState?) { + if (itemState != null && itemState.playerState != PlayerState.COMPLETED && itemState.file.id == playbackFile?.id) { + max = itemState.maxTimeInMilliseconds + progress = itemState.currentTimeInMilliseconds + visibility = VISIBLE + } else { + visibility = GONE + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/PlayerControl.kt b/app/src/main/java/com/ionos/player/ui/control/PlayerControl.kt new file mode 100644 index 000000000000..4992083239ab --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/PlayerControl.kt @@ -0,0 +1,61 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control + +import com.ionos.player.ui.common.PlayerBasePresenter + +interface PlayerControl { + + interface View { + + fun repeat() + + fun doNotRepeat() + + fun shuffle() + + fun doNotShuffle() + + fun setProgress(currentTimeInMilliseconds: Int, totalTimeInMilliseconds: Int) + + fun setProgressAvailable() + + fun setProgressNotAvailable() + + fun enablePlayControls(play: Boolean, pause: Boolean, stop: Boolean) + + fun enableSwitchControls(next: Boolean, previous: Boolean) + } + + interface Presenter : PlayerBasePresenter { + + fun setView(view: View?) + + fun onPlay() + + fun onPause() + + fun onStop() + + fun onNextClicked() + + fun onPreviousClicked() + + fun onPreviousDoubleClicked() + + fun onSeekToPosition(positionInMilliseconds: Int) + + fun onRepeat() + + fun onDoNotRepeat() + + fun onShuffle() + + fun onDoNotShuffle() + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/PlayerControlPresenter.kt b/app/src/main/java/com/ionos/player/ui/control/PlayerControlPresenter.kt new file mode 100644 index 000000000000..65d146e6b769 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/PlayerControlPresenter.kt @@ -0,0 +1,161 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control + +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.state.PlaybackItemState +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.PlayerState +import com.ionos.player.model.state.PlayerStateAnalyzer +import com.ionos.player.model.state.RepeatMode +import kotlin.jvm.optionals.getOrNull + +class PlayerControlPresenter( + private val model: PlaybackModel +) : PlayerControl.Presenter { + private var view: PlayerControl.View? = null + + private val currentPlaybackItemState: PlaybackItemState? + get() = model.state.getOrNull()?.currentItemState + + override fun setView(view: PlayerControl.View?) { + this.view = view + } + + override fun onCreate() { + updateView() + } + + override fun onDestroy() { + view = null + } + + override fun onAppear() { + updateView() + model.addListener(listener) + } + + override fun onDisappear() { + model.removeListener(listener) + } + + override fun onPlay() { + model.play() + } + + override fun onPause() { + model.pause() + } + + override fun onStop() { + model.stop() + } + + override fun onNextClicked() { + model.playNext() + } + + override fun onPreviousClicked() { + val state = currentPlaybackItemState ?: return + if (state.playerState == PlayerState.PAUSED || state.playerState == PlayerState.PLAYING) { + model.seekToPosition(0) + } else { + model.playPrevious() + } + } + + override fun onPreviousDoubleClicked() { + val state = currentPlaybackItemState ?: return + model.playPrevious() + if (state.playerState != PlayerState.PAUSED && state.playerState != PlayerState.PLAYING) { + model.playPrevious() + } + } + + override fun onSeekToPosition(positionInMilliseconds: Int) { + model.seekToPosition(positionInMilliseconds) + } + + override fun onRepeat() { + model.setRepeatMode(RepeatMode.SINGLE) + } + + override fun onDoNotRepeat() { + model.setRepeatMode(RepeatMode.ALL) + } + + override fun onShuffle() { + model.setShuffle(true) + } + + override fun onDoNotShuffle() { + model.setShuffle(false) + } + + private fun updateView() { + val state = model.state + + var repeatSingle = false + var shuffle = false + + if (state.isPresent) { + repeatSingle = state.get().repeatMode == RepeatMode.SINGLE + shuffle = state.get().shuffle + } + + if (repeatSingle) { + view?.repeat() + } else { + view?.doNotRepeat() + } + + if (shuffle) { + view?.shuffle() + } else { + view?.doNotShuffle() + } + + val playbackState = state.getOrNull()?.currentItemState + if (playbackState != null) { + val analyzer = PlayerStateAnalyzer(playbackState.playerState) + + view?.enablePlayControls( + analyzer.playAvailable(), + analyzer.pauseAvailable(), + analyzer.stopAvailable(), + ) + + view?.enableSwitchControls( + state.get().currentFiles.size > 1, + state.get().currentFiles.isNotEmpty(), + ) + + if (playbackState.maxTimeInMilliseconds > 0) { + val currentTime = playbackState.currentTimeInMilliseconds + val maxTime = playbackState.maxTimeInMilliseconds + view?.setProgressAvailable() + view?.setProgress(currentTime, maxTime) + } else { + view?.setProgressNotAvailable() + } + } else { + view?.enablePlayControls(false, false, false) + view?.enableSwitchControls(false, false) + view?.setProgressNotAvailable() + } + } + + private val listener: PlaybackModel.Listener = object : PlaybackModel.Listener { + override fun onPlaybackUpdate(state: PlaybackState) { + updateView() + } + + override fun onPlaybackError(error: Throwable) { + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/PlayerControlView.java b/app/src/main/java/com/ionos/player/ui/control/PlayerControlView.java new file mode 100644 index 000000000000..4c36f34261f7 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/PlayerControlView.java @@ -0,0 +1,329 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.ionos.player.model.PlaybackModel; +import com.ionos.player.ui.control.listener.MultipleClickListener; +import com.ionos.player.ui.control.listener.PlayerControlViewCompositeListener; +import com.ionos.player.ui.control.listener.PlayerControlViewListener; +import com.owncloud.android.R; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.core.content.ContextCompat; +import androidx.core.widget.ImageViewCompat; +import dagger.android.HasAndroidInjector; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; +import io.reactivex.subjects.PublishSubject; + +public class PlayerControlView extends LinearLayout implements PlayerControl.View { + + private static final String INDETERMINATE_TIME = "--:--"; + + private static final String TAG_CLICK_COMMAND_PLAY = "TAG_CLICK_COMMAND_PLAY"; + private static final String TAG_CLICK_COMMAND_PAUSE = "TAG_CLICK_COMMAND_PAUSE"; + + private static final String TAG_CLICK_COMMAND_REPEAT = "TAG_CLICK_COMMAND_REPEAT"; + private static final String TAG_CLICK_COMMAND_DO_NOT_REPEAT = "TAG_CLICK_COMMAND_DO_NOT_REPEAT"; + + private static final String TAG_CLICK_COMMAND_SHUFFLE = "TAG_CLICK_COMMAND_SHUFFLE"; + private static final String TAG_CLICK_COMMAND_DO_NOT_SHUFFLE = "TAG_CLICK_COMMAND_DO_NOT_SHUFFLE"; + + private static final String TAG_CLICK_COMMAND_UNKNOWN = "TAG_CLICK_COMMAND_UNKNOWN"; + + @Inject + PlaybackModel playerModel; + + private PlayerControl.Presenter controlPresenter; + private final PublishSubject seekBarProgressChangePublishSubject = PublishSubject.create(); + private Disposable seekBarProgressChangeDisposable = Disposables.disposed(); + + private final TextView tvElapsed; + private final TextView tvTotalTime; + private final SeekBar progressBar; + private final ImageView ivRandom; + private final ImageView ivRepeat; + private final ImageView ivPrevious; + private final ImageView ivNext; + private final ImageView ivPlayPause; + + private final PlayerControlViewCompositeListener compositeListener = new PlayerControlViewCompositeListener(); + + public PlayerControlView(Context context) { + this(context, null); + } + + public PlayerControlView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + inflate(context, R.layout.player_control_view, this); + this.tvElapsed = findViewById(R.id.tvElapsed); + this.tvTotalTime = findViewById(R.id.tvTotalTime); + this.progressBar = findViewById(R.id.progressBar); + this.ivRandom = findViewById(R.id.ivRandom); + this.ivRepeat = findViewById(R.id.ivRepeat); + this.ivPrevious = findViewById(R.id.ivPrevious); + this.ivNext = findViewById(R.id.ivNext); + this.ivPlayPause = findViewById(R.id.ivPlayPause); + + setProgress(0, 0); + + if (isInEditMode()) { + return; + } + + ((HasAndroidInjector) context.getApplicationContext()).androidInjector().inject(this); + + this.controlPresenter = new PlayerControlPresenter(this.playerModel); + + setDefaultTags(); + setListeners(); + } + + public void addListener(PlayerControlViewListener listener) { + this.compositeListener.addListener(listener); + } + + public void removeListener(PlayerControlViewListener listener) { + this.compositeListener.removeListener(listener); + } + + private void setDefaultTags() { + this.ivPlayPause.setTag(TAG_CLICK_COMMAND_UNKNOWN); + this.ivRandom.setTag(TAG_CLICK_COMMAND_UNKNOWN); + this.ivRepeat.setTag(TAG_CLICK_COMMAND_UNKNOWN); + } + + private void setListeners() { + this.ivPlayPause.setOnClickListener(view -> handlePlayPauseClick()); + this.ivNext.setOnClickListener(view -> { + controlPresenter.onNextClicked(); + compositeListener.onNextClicked(); + }); + this.ivPrevious.setOnClickListener(new MultipleClickListener() { + @Override + protected void onSingleClick(View view) { + controlPresenter.onPreviousClicked(); + compositeListener.onPreviousClicked(); + } + + @Override + protected void onDoubleClick(View view) { + controlPresenter.onPreviousDoubleClicked(); + compositeListener.onPreviousClicked(); + } + }); + + this.ivRepeat.setOnClickListener(view -> handleRepeatClick()); + this.ivRandom.setOnClickListener(view -> handleShuffleClick()); + this.progressBar.setOnSeekBarChangeListener(this.onSeekBarChangeListener); + } + + private void subscribeToSeekBarProgressChange() { + seekBarProgressChangeDisposable = seekBarProgressChangePublishSubject + .debounce(200, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(progress -> { + controlPresenter.onSeekToPosition(progress); + compositeListener.onProgressChangedByUser(progress); + }); + } + + private void unsubscribeFromSeekBarProgressChange() { + seekBarProgressChangeDisposable.dispose(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (isInEditMode()) { + return; + } + this.controlPresenter.setView(this); + this.controlPresenter.onCreate(); + } + + @Override + protected void onDetachedFromWindow() { + if (isInEditMode()) { + return; + } + this.controlPresenter.onDestroy(); + this.controlPresenter.setView(null); + unsubscribeFromSeekBarProgressChange(); + super.onDetachedFromWindow(); + } + + public void onStart() { + subscribeToSeekBarProgressChange(); + this.controlPresenter.onAppear(); + } + + public void onStop() { + unsubscribeFromSeekBarProgressChange(); + this.controlPresenter.onDisappear(); + } + + private final SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + seekBarProgressChangePublishSubject.onNext(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + compositeListener.onProgressStopTrackingTouch(); + } + }; + + private void handlePlayPauseClick() { + if (TAG_CLICK_COMMAND_PLAY.equals(ivPlayPause.getTag())) { + controlPresenter.onPlay(); + compositeListener.onPlayClicked(); + } else if (TAG_CLICK_COMMAND_PAUSE.equals(ivPlayPause.getTag())) { + controlPresenter.onPause(); + compositeListener.onPauseClicked(); + } else { + Log.w(getClass().getSimpleName(), "Unreachable playPause button state"); + } + } + + private void handleRepeatClick() { + if (TAG_CLICK_COMMAND_REPEAT.equals(ivRepeat.getTag())) { + controlPresenter.onRepeat(); + compositeListener.onRepeatClicked(); + } else if (TAG_CLICK_COMMAND_DO_NOT_REPEAT.equals(ivRepeat.getTag())) { + controlPresenter.onDoNotRepeat(); + compositeListener.onDoNotRepeatClicked(); + } else { + Log.w(getClass().getSimpleName(), "Unreachable repeat button state"); + } + } + + private void handleShuffleClick() { + if (TAG_CLICK_COMMAND_SHUFFLE.equals(ivRandom.getTag())) { + controlPresenter.onShuffle(); + compositeListener.onShuffleClicked(); + } else if (TAG_CLICK_COMMAND_DO_NOT_SHUFFLE.equals(ivRandom.getTag())) { + controlPresenter.onDoNotShuffle(); + compositeListener.onDoNotShuffleClicked(); + } else { + Log.w(getClass().getSimpleName(), "Unreachable shuffle button state"); + } + } + + @Override + public void repeat() { + setImageTint(ivRepeat, R.color.player_accent_color); + ivRepeat.setTag(TAG_CLICK_COMMAND_DO_NOT_REPEAT); + } + + @Override + public void doNotRepeat() { + setImageTint(ivRepeat, R.color.player_default_icon_color); + ivRepeat.setTag(TAG_CLICK_COMMAND_REPEAT); + } + + @Override + public void shuffle() { + setImageTint(ivRandom, R.color.player_accent_color); + ivRandom.setTag(TAG_CLICK_COMMAND_DO_NOT_SHUFFLE); + } + + @Override + public void doNotShuffle() { + setImageTint(ivRandom, R.color.player_default_icon_color); + ivRandom.setTag(TAG_CLICK_COMMAND_SHUFFLE); + } + + @Override + public void setProgress(int currentTimeInMilliseconds, int totalTimeInMilliseconds) { + progressBar.setMax(totalTimeInMilliseconds); + progressBar.setProgress(currentTimeInMilliseconds); + tvElapsed.setText(formatTime(currentTimeInMilliseconds)); + tvTotalTime.setText(formatTime(totalTimeInMilliseconds)); + } + + @Override + public void setProgressAvailable() { + progressBar.setEnabled(true); + } + + @Override + public void setProgressNotAvailable() { + progressBar.setEnabled(false); + progressBar.setMax(100); + progressBar.setProgress(0); + tvElapsed.setText(INDETERMINATE_TIME); + tvTotalTime.setText(INDETERMINATE_TIME); + } + + @Override + public void enablePlayControls(boolean play, boolean pause, boolean stop) { + if (pause) { + configurePlayPauseButton(R.drawable.player_ic_pause, TAG_CLICK_COMMAND_PAUSE); + } else { + configurePlayPauseButton(R.drawable.player_ic_play, TAG_CLICK_COMMAND_PLAY); + } + } + + @Override + public void enableSwitchControls(boolean next, boolean previous) { + ivNext.setEnabled(next); + ivPrevious.setEnabled(previous); + } + + private void configurePlayPauseButton(@DrawableRes int imageResource, String tagState) { + ivPlayPause.setImageResource(imageResource); + ivPlayPause.setTag(tagState); + } + + private void setImageTint(ImageView imageView, @ColorRes int colorRes) { + int color = ContextCompat.getColor(getContext(), colorRes); + ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(color)); + } + + private String formatTime(int timeMillis) { + int timeSeconds = timeMillis / 1000; + int seconds = timeSeconds % 60; + int minutes = timeSeconds / 60; + int hours = minutes / 60; + if (hours > 0) { + return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes % 60, seconds); + } else { + return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds); + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/listener/MultipleClickListener.java b/app/src/main/java/com/ionos/player/ui/control/listener/MultipleClickListener.java new file mode 100644 index 000000000000..e30520aaff91 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/listener/MultipleClickListener.java @@ -0,0 +1,52 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control.listener; + +import android.os.Handler; +import android.view.View; + +import java.util.Optional; + +public abstract class MultipleClickListener implements View.OnClickListener { + + private static final int TIME_WINDOW_FOR_CLICK_DETERMINATION_IN_MILLISECONDS = 250; + + private final Handler handler = new Handler(); + private Optional clicksCount = Optional.empty(); + + protected abstract void onSingleClick(View view); + + protected abstract void onDoubleClick(View view); + + @Override + public final void onClick(final View view) { + boolean interactionIsBegan = clicksCount.isPresent(); + + if (interactionIsBegan) { + clicksCount = Optional.of(clicksCount.get() + 1); + } else { + clicksCount = Optional.of(1); + handler.postDelayed(new Runnable() { + @Override + public void run() { + int count = clicksCount.get(); + clicksCount = Optional.empty(); + callSubscriber(view, count); + } + }, TIME_WINDOW_FOR_CLICK_DETERMINATION_IN_MILLISECONDS); + } + } + + private void callSubscriber(View view, int clicksCount) { + if (clicksCount == 1) { + onSingleClick(view); + } else { + onDoubleClick(view); + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewCompositeListener.java b/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewCompositeListener.java new file mode 100644 index 000000000000..92089da5e219 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewCompositeListener.java @@ -0,0 +1,94 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control.listener; + +import java.util.HashSet; +import java.util.Set; + +public class PlayerControlViewCompositeListener implements PlayerControlViewListener { + + private final Set listeners = new HashSet<>(); + + public void addListener(PlayerControlViewListener listener) { + this.listeners.add(listener); + } + + public void removeListener(PlayerControlViewListener listener) { + this.listeners.remove(listener); + } + + @Override + public void onNextClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onNextClicked(); + } + } + + @Override + public void onPreviousClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onPreviousClicked(); + } + } + + @Override + public void onPlayClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onPlayClicked(); + } + } + + @Override + public void onPauseClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onPauseClicked(); + } + } + + @Override + public void onRepeatClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onRepeatClicked(); + } + } + + @Override + public void onDoNotRepeatClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onDoNotRepeatClicked(); + } + } + + @Override + public void onShuffleClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onShuffleClicked(); + } + } + + @Override + public void onDoNotShuffleClicked() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onDoNotShuffleClicked(); + } + } + + @Override + public void onProgressChangedByUser(int progress) { + for (PlayerControlViewListener listener : this.listeners) { + listener.onProgressChangedByUser(progress); + } + } + + @Override + public void onProgressStopTrackingTouch() { + for (PlayerControlViewListener listener : this.listeners) { + listener.onProgressStopTrackingTouch(); + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewListener.java b/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewListener.java new file mode 100644 index 000000000000..0fc979b3815b --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/control/listener/PlayerControlViewListener.java @@ -0,0 +1,31 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.control.listener; + +public interface PlayerControlViewListener { + + void onNextClicked(); + + void onPreviousClicked(); + + void onPlayClicked(); + + void onPauseClicked(); + + void onRepeatClicked(); + + void onDoNotRepeatClicked(); + + void onShuffleClicked(); + + void onDoNotShuffleClicked(); + + void onProgressChangedByUser(int progress); + + void onProgressStopTrackingTouch(); +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/PlayerPager.java b/app/src/main/java/com/ionos/player/ui/pager/PlayerPager.java new file mode 100644 index 000000000000..bfb3ba4128e9 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/PlayerPager.java @@ -0,0 +1,332 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import com.ionos.player.ui.pager.adapter.AbstractFragmentPagerAdapter; +import com.ionos.player.ui.pager.adapter.DefaultFragmentPagerAdapter; +import com.ionos.player.ui.pager.adapter.InfiniteFragmentPagerAdapter; +import com.ionos.player.ui.pager.listener.NullPlayerPagerListener; +import com.ionos.player.ui.pager.listener.OnPageChangeCompositeListener; +import com.ionos.player.ui.pager.listener.PlayerPagerListener; +import com.owncloud.android.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import androidx.fragment.app.FragmentManager; +import androidx.viewpager.widget.ViewPager; + +public class PlayerPager extends LinearLayout { + + private final ViewPager viewPager; + private ModeStrategy modeStrategy; + private AbstractFragmentPagerAdapter adapter; + private PlayerPagerListener playerPagerListener = NullPlayerPagerListener.getInstance(); + private final OnPageChangeCompositeListener onPageChangeCompositeListener = new OnPageChangeCompositeListener(); + private int currentPosition = -1; + private int shift = -1; + private int restoredShift = -1; + + private ViewPager.OnPageChangeListener listener; + + public PlayerPager(Context context) { + this(context, null); + } + + public PlayerPager(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(context, R.layout.player_pager, this); + this.viewPager = findViewById(R.id.viewPager); + listener = nullOnPageChangeListener; + } + + public void setPlayerPagerListener(PlayerPagerListener playerPagerListener) { + this.playerPagerListener = playerPagerListener != null ? playerPagerListener : NullPlayerPagerListener.getInstance(); + } + + public void addOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + this.listener = listener; + this.onPageChangeCompositeListener.addListener(this.listener); + } + + public void removeOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + this.onPageChangeCompositeListener.removeListener(listener); + } + + public int getCurrentPosition() { + return this.viewPager.getCurrentItem(); + } + + public T getCurrentItem() { + return adapter.getItemForPosition(getCurrentPosition()); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable state = super.onSaveInstanceState(); + InfiniteViewPagerState infiniteViewPagerState = new InfiniteViewPagerState(state); + infiniteViewPagerState.shiftedPosition = this.shift; + return infiniteViewPagerState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + InfiniteViewPagerState restoredState = (InfiniteViewPagerState) state; + super.onRestoreInstanceState(restoredState.getSuperState()); + + this.restoredShift = restoredState.shiftedPosition; + } + + public void init(FragmentManager fragmentManager, PlayerPagerMode mode, PlayerPagerFragmentFactory fragmentFactory) { + this.modeStrategy = createModeStrategy(mode); + this.adapter = this.modeStrategy.createAdapter(fragmentManager, fragmentFactory); + this.viewPager.setAdapter(this.adapter); + this.onPageChangeCompositeListener.addListener(this.modeStrategy.createListener()); + } + + private ModeStrategy createModeStrategy(PlayerPagerMode mode) { + switch (mode) { + case DEFAULT: + return new FiniteModeStrategy(); + case INFINITE: + return new InfiniteModeStrategy(); + default: + throw new RuntimeException("Unknown value " + mode); + } + } + + public List getItems() { + return this.adapter.getItems(); + } + + public void setItems(List items) { + final boolean shiftHasBeenRestored = restoredShift != -1; + if (shiftHasBeenRestored) { + items = shiftRestoredPosition(items); + } + + final int calculatedCurrentPositionWithOffsetIfNeeded = modeStrategy.getCurrentPosition(adapter.getCount(), currentPosition); + T currentItem = null; + if (calculatedCurrentPositionWithOffsetIfNeeded >= 0 && currentItemPositionsNotTheSameAfterShuffleMatch(calculatedCurrentPositionWithOffsetIfNeeded)) { + currentItem = adapter.getItems().get(calculatedCurrentPositionWithOffsetIfNeeded); + items = calculateShiftAndRotateList(items, calculatedCurrentPositionWithOffsetIfNeeded, currentItem); + } + + this.adapter.setItems(items); + if (currentItem != null) { + this.adapter.setCurrentItem(!items.isEmpty() ? currentItem : null); + } + notifyDataSetChangedWithoutCallingListener(); + setCurrentItem(currentItem, false); + } + + private List calculateShiftAndRotateList(List items, int calculatedCurrentPositionWithOffsetForInfinityStrategy, T currentItem) { + this.shift = calculateShift(items, calculatedCurrentPositionWithOffsetForInfinityStrategy, currentItem); + items = rotate(items, this.shift); + return items; + } + + private void notifyDataSetChangedWithoutCallingListener() { + this.viewPager.removeOnPageChangeListener(this.onPageChangeCompositeListener); + this.adapter.notifyDataSetChanged(); + this.viewPager.addOnPageChangeListener(this.onPageChangeCompositeListener); + } + + private boolean currentItemPositionsNotTheSameAfterShuffleMatch(int calculatedCurrentPosition) { + return !this.adapter.getItems().isEmpty() && + this.currentPosition >= 0 && + calculatedCurrentPosition < this.adapter.getItems().size(); + } + + private List shiftRestoredPosition(List items) { + this.shift = this.restoredShift; + items = rotate(items, this.shift); + this.restoredShift = -1; + return items; + } + + static int calculateShift(List items, int calculatedCurrentPosition, Object currentItem) { + int newCurrentItemIndex = items.indexOf(currentItem); + if (newCurrentItemIndex >= 0) { + if (newCurrentItemIndex != calculatedCurrentPosition) { + return items.size() - newCurrentItemIndex + calculatedCurrentPosition; + } + } + return 0; + } + + static List rotate(List list, int shift) { + List newValues = new ArrayList<>(list); + Collections.rotate(newValues, shift); + return newValues; + } + + public void setCurrentItem(T item) { + setCurrentItem(item, true); + } + + private void setCurrentItem(T item, boolean smoothScroll) { + this.currentPosition = this.adapter.getEntityIndex(item); + if (this.currentPosition != -1 && this.viewPager.getCurrentItem() != this.currentPosition) { + this.viewPager.removeOnPageChangeListener(this.onPageChangeCompositeListener); + this.viewPager.setCurrentItem(this.currentPosition, smoothScroll); + this.viewPager.addOnPageChangeListener(this.onPageChangeCompositeListener); + } + } + + public void setCurrentItem(int position) { + this.viewPager.setCurrentItem(position); + } + + //region OnPageChangeListener + + public class DefaultOnPageChangeListener implements ViewPager.OnPageChangeListener { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + playerPagerListener.onSwitchToItem(adapter.getItemForPosition(position)); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + } + + public class InfinityOnPageChangeListener implements ViewPager.OnPageChangeListener { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + if (position == 0) { + onPageChangeCompositeListener.removeListener(listener); + viewPager.setCurrentItem(adapter.getCount() - 2, false); + onPageChangeCompositeListener.addListener(listener); + return; + } + + if (position >= adapter.getCount() - 1) { + onPageChangeCompositeListener.removeListener(listener); + viewPager.setCurrentItem(1, false); + onPageChangeCompositeListener.addListener(listener); + return; + } + playerPagerListener.onSwitchToItem(adapter.getItemForPosition(position)); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + } + //endregion + + //region Strategy + + private interface ModeStrategy { + AbstractFragmentPagerAdapter createAdapter(FragmentManager fragmentManager, PlayerPagerFragmentFactory fragmentFactory); + + ViewPager.OnPageChangeListener createListener(); + + int getCurrentPosition(int itemCount, int position); + } + + private class FiniteModeStrategy implements ModeStrategy { + @Override + public AbstractFragmentPagerAdapter createAdapter(FragmentManager fragmentManager, PlayerPagerFragmentFactory fragmentFactory) { + return new DefaultFragmentPagerAdapter<>(fragmentManager, fragmentFactory); + } + + @Override + public ViewPager.OnPageChangeListener createListener() { + return new DefaultOnPageChangeListener(); + } + + @Override + public int getCurrentPosition(int itemCount, int position) { + return position; + } + } + + private class InfiniteModeStrategy implements ModeStrategy { + @Override + public AbstractFragmentPagerAdapter createAdapter(FragmentManager fragmentManager, PlayerPagerFragmentFactory fragmentFactory) { + return new InfiniteFragmentPagerAdapter<>(fragmentManager, fragmentFactory); + } + + @Override + public ViewPager.OnPageChangeListener createListener() { + return new InfinityOnPageChangeListener(); + } + + @Override + public int getCurrentPosition(int itemCount, int position) { + return itemCount > 1 ? position - 1 : position; + } + } +//endregion + + private static class InfiniteViewPagerState extends BaseSavedState { + private int shiftedPosition; + + InfiniteViewPagerState(Parcelable superState) { + super(superState); + } + + private InfiniteViewPagerState(Parcel in) { + super(in); + this.shiftedPosition = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.shiftedPosition); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public InfiniteViewPagerState createFromParcel(Parcel in) { + return new InfiniteViewPagerState(in); + } + + public InfiniteViewPagerState[] newArray(int size) { + return new InfiniteViewPagerState[size]; + } + }; + } + + private final ViewPager.OnPageChangeListener nullOnPageChangeListener = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }; +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerFragmentFactory.java b/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerFragmentFactory.java new file mode 100644 index 000000000000..22ef5c7ffe5c --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerFragmentFactory.java @@ -0,0 +1,14 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager; + +import androidx.fragment.app.Fragment; + +public interface PlayerPagerFragmentFactory { + Fragment create(T t); +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerMode.java b/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerMode.java new file mode 100644 index 000000000000..ffac52a8d545 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/PlayerPagerMode.java @@ -0,0 +1,13 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager; + +public enum PlayerPagerMode { + DEFAULT, + INFINITE +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/adapter/AbstractFragmentPagerAdapter.java b/app/src/main/java/com/ionos/player/ui/pager/adapter/AbstractFragmentPagerAdapter.java new file mode 100644 index 000000000000..08bb9385c836 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/adapter/AbstractFragmentPagerAdapter.java @@ -0,0 +1,101 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.adapter; + +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +import androidx.core.util.Pair; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter { + + protected List items = new ArrayList<>(); + protected T currentItem; + protected final List> cachedItems = new ArrayList<>(); + + AbstractFragmentPagerAdapter(FragmentManager fm) { + super(fm); + } + + abstract public int getEntityIndex(T entity); + + abstract public List getItems(); + + abstract public void setItems(List items); + + abstract protected T getLinkedItem(int position); + + public void setCurrentItem(T item) { + this.currentItem = item; + } + + + @Override + public Object instantiateItem(ViewGroup container, int position) { + final Fragment fragment = (Fragment) super.instantiateItem(container, position); + T linkedItem = getLinkedItem(position); + if (!findAndReplace(fragment, linkedItem)) { + this.cachedItems.add(new Pair<>(linkedItem, fragment)); + } + return fragment; + } + + private boolean findAndReplace(Fragment fragment, T linkedItem) { + for (int i = 0; i < this.cachedItems.size(); i++) { + final Pair pair = this.cachedItems.get(i); + if (pair.first.equals(linkedItem)) { + Pair newPair = new Pair<>(pair.first, fragment); + this.cachedItems.add(newPair); + this.cachedItems.remove(pair); + return true; + } + } + return false; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + for (int i = 0; i < this.cachedItems.size(); i++) { + final Pair pair = this.cachedItems.get(i); + if (pair.second.equals(object)) { + this.cachedItems.remove(pair); + break; + } + } + super.destroyItem(container, position, object); + } + + public T getItemForPosition(int position) { + return this.items.get(position); + } + + @Override + public int getItemPosition(Object object) { + for (int i = 0; i < this.cachedItems.size(); i++) { + final Pair pair = this.cachedItems.get(i); + if (pair.second.equals(object)) { + if (this.currentItem != null && this.currentItem.equals(pair.first)) { + return super.getItemPosition(object); + } else { + return POSITION_NONE; + } + } + } + return POSITION_NONE; + } + + @Override + public int getCount() { + return this.items.size(); + } +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/adapter/DefaultFragmentPagerAdapter.java b/app/src/main/java/com/ionos/player/ui/pager/adapter/DefaultFragmentPagerAdapter.java new file mode 100644 index 000000000000..a83995da4d7b --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/adapter/DefaultFragmentPagerAdapter.java @@ -0,0 +1,47 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.adapter; + +import com.ionos.player.ui.pager.PlayerPagerFragmentFactory; + +import java.util.List; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +public class DefaultFragmentPagerAdapter extends AbstractFragmentPagerAdapter { + private final PlayerPagerFragmentFactory fragmentFactory; + + public DefaultFragmentPagerAdapter(FragmentManager fragmentManager, PlayerPagerFragmentFactory fragmentFactory) { + super(fragmentManager); + this.fragmentFactory = fragmentFactory; + } + + @Override + public int getEntityIndex(T entity) { + return getItems().indexOf(entity); + } + + @Override + public Fragment getItem(int position) { + return this.fragmentFactory.create(this.items.get(position)); + } + + @Override + protected T getLinkedItem(int position) { + return this.items.get(position); + } + + public List getItems() { + return this.items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/adapter/InfiniteFragmentPagerAdapter.java b/app/src/main/java/com/ionos/player/ui/pager/adapter/InfiniteFragmentPagerAdapter.java new file mode 100644 index 000000000000..3198aab481db --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/adapter/InfiniteFragmentPagerAdapter.java @@ -0,0 +1,88 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.adapter; + +import com.ionos.player.ui.pager.PlayerPagerFragmentFactory; + +import java.util.ArrayList; +import java.util.List; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +public class InfiniteFragmentPagerAdapter extends AbstractFragmentPagerAdapter { + + private final PlayerPagerFragmentFactory playerSourceFragmentFactory; + + public InfiniteFragmentPagerAdapter(FragmentManager fragmentManager, PlayerPagerFragmentFactory playerSourceFragmentFactory) { + super(fragmentManager); + this.playerSourceFragmentFactory = playerSourceFragmentFactory; + } + + @Override + public Fragment getItem(int position) { + return this.playerSourceFragmentFactory.create(this.items.get(position)); + } + + @Override + protected T getLinkedItem(int position) { + T linkedItem; + if (position == 0) { + linkedItem = getItems().get(getItems().size() - 1); + } else if (position == getItems().size() + 1) { + linkedItem = getItems().get(0); + } else { + linkedItem = getItems().get(position - 1); + } + return linkedItem; + } + + @Override + public List getItems() { + return this.items.size() > 1 ? removeStubs(this.items) : this.items; + } + + @Override + public void setItems(List items) { + this.items = new ArrayList<>(items); + if (items.size() > 1) { + this.items = addStubs(this.items); + } + notifyDataSetChanged(); + } + + @Override + public int getEntityIndex(T entity) { + List items = new ArrayList<>(this.items); + if (items.size() > 1) { + items = removeStubs(items); + int songIndex = items.indexOf(entity); + if (songIndex != -1) { + return songIndex + 1; + } else { + return songIndex; + } + } else { + return items.indexOf(entity); + } + } + + private List addStubs(List sources) { + List result = new ArrayList<>(sources); + result.add(0, result.get(result.size() - 1)); + result.add(result.get(1)); + return result; + } + + private List removeStubs(List sources) { + List result = new ArrayList<>(sources); + result.remove(0); + result.remove(result.size() - 1); + return result; + } +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/listener/NullPlayerPagerListener.java b/app/src/main/java/com/ionos/player/ui/pager/listener/NullPlayerPagerListener.java new file mode 100644 index 000000000000..ddf837afb202 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/listener/NullPlayerPagerListener.java @@ -0,0 +1,21 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.listener; + +public class NullPlayerPagerListener implements PlayerPagerListener { + + private static final NullPlayerPagerListener INSTANCE = new NullPlayerPagerListener(); + + public static NullPlayerPagerListener getInstance() { + return INSTANCE; + } + + @Override + public void onSwitchToItem(T item) { + } +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/listener/OnPageChangeCompositeListener.java b/app/src/main/java/com/ionos/player/ui/pager/listener/OnPageChangeCompositeListener.java new file mode 100644 index 000000000000..911aa9f09540 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/listener/OnPageChangeCompositeListener.java @@ -0,0 +1,47 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.listener; + +import java.util.HashSet; +import java.util.Set; + +import androidx.viewpager.widget.ViewPager; + +public class OnPageChangeCompositeListener implements ViewPager.OnPageChangeListener { + + private final Set listeners = new HashSet<>(); + + public void addListener(ViewPager.OnPageChangeListener listener) { + this.listeners.add(listener); + } + + public void removeListener(ViewPager.OnPageChangeListener listener) { + this.listeners.remove(listener); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + for (ViewPager.OnPageChangeListener listener : new HashSet<>(this.listeners)) { + listener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + for (ViewPager.OnPageChangeListener listener : new HashSet<>(this.listeners)) { + listener.onPageSelected(position); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + for (ViewPager.OnPageChangeListener listener : new HashSet<>(this.listeners)) { + listener.onPageScrollStateChanged(state); + } + } +} diff --git a/app/src/main/java/com/ionos/player/ui/pager/listener/PlayerPagerListener.java b/app/src/main/java/com/ionos/player/ui/pager/listener/PlayerPagerListener.java new file mode 100644 index 000000000000..76890045f7d0 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/pager/listener/PlayerPagerListener.java @@ -0,0 +1,12 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.pager.listener; + +public interface PlayerPagerListener { + void onSwitchToItem(T item); +} diff --git a/app/src/main/java/com/ionos/player/ui/video/VideoFileFragment.kt b/app/src/main/java/com/ionos/player/ui/video/VideoFileFragment.kt new file mode 100644 index 000000000000..b8ee18cba1bf --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/video/VideoFileFragment.kt @@ -0,0 +1,132 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.video + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.ionos.player.model.PlaybackFile +import com.ionos.player.model.PlaybackModel +import com.ionos.player.model.ThumbnailLoader +import com.ionos.player.model.state.PlaybackState +import com.ionos.player.model.state.VideoSize +import com.ionos.player.util.getDisplayHeight +import com.ionos.player.util.getDisplayWidth +import com.owncloud.android.R +import com.owncloud.android.databinding.PlayerVideoFileFragmentBinding +import dagger.android.support.AndroidSupportInjection +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +class VideoFileFragment : Fragment(), PlaybackModel.Listener { + + companion object { + private const val ARGUMENT_FILE = "ARGUMENT_FILE" + + fun createInstance(file: PlaybackFile) = VideoFileFragment().apply { + arguments = bundleOf(ARGUMENT_FILE to file) + } + } + + @Inject + lateinit var playerModel: PlaybackModel + + @Inject + lateinit var thumbnailLoader: ThumbnailLoader + + private lateinit var file: PlaybackFile + + private lateinit var binding: PlayerVideoFileFragmentBinding + + private var previousVideoSize: VideoSize? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidSupportInjection.inject(this) + this.file = arguments?.getSerializable(ARGUMENT_FILE) as PlaybackFile + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = PlayerVideoFileFragmentBinding.inflate(inflater, container, false) + loadFileThumbnail() + return binding.root + } + + override fun onStart() { + super.onStart() + render(playerModel.state.getOrNull()) + playerModel.addListener(this) + } + + override fun onStop() { + playerModel.removeListener(this) + super.onStop() + } + + override fun onPlaybackUpdate(state: PlaybackState) { + render(state) + } + + override fun onPlaybackError(error: Throwable) { + } + + private fun loadFileThumbnail() { + viewLifecycleOwner.lifecycleScope.launch { + val context = context ?: return@launch + val thumbnailSize = context.resources.getDimension(R.dimen.player_album_cover_size) + val thumbnail = thumbnailLoader.await(context, file, thumbnailSize.toInt(), thumbnailSize.toInt()) + thumbnail?.let(binding.thumbnail::setImageBitmap) + } + } + + private fun render(state: PlaybackState?) { + val currentItemState = state?.currentItemState + if (currentItemState?.file == file) { + showVideo(currentItemState.videoSize) + } else { + binding.surfaceView.visibility = View.GONE + if (currentItemState == null) { + playerModel.setVideoSurfaceView(null) + } + } + } + + private fun showVideo(videoSize: VideoSize?) { + playerModel.setVideoSurfaceView(binding.surfaceView) + binding.surfaceView.visibility = View.VISIBLE + binding.surfaceView.alpha = if (videoSize != null) 1f else 0f + + if (videoSize != null && previousVideoSize != videoSize) { + previousVideoSize = videoSize + setVideoSize(videoSize.width, videoSize.height) + } + } + + private fun setVideoSize(videoWidth: Int, videoHeight: Int) { + val screenWidth = requireContext().getDisplayWidth() + val screenHeight = requireContext().getDisplayHeight() + val screenProportion = screenWidth.toFloat() / screenHeight.toFloat() + val videoProportion = videoWidth.toFloat() / videoHeight.toFloat() + + val layoutParams = binding.surfaceView.layoutParams + if (screenProportion < videoProportion) { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = (screenWidth.toFloat() / videoProportion).toInt() + } else { + layoutParams.width = (videoProportion * screenHeight.toFloat()).toInt() + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + } + + binding.surfaceView.layoutParams = layoutParams + } +} diff --git a/app/src/main/java/com/ionos/player/ui/video/VideoFileFragmentFactory.kt b/app/src/main/java/com/ionos/player/ui/video/VideoFileFragmentFactory.kt new file mode 100644 index 000000000000..bbef03a34791 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/video/VideoFileFragmentFactory.kt @@ -0,0 +1,19 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.video + +import androidx.fragment.app.Fragment +import com.ionos.player.model.PlaybackFile +import com.ionos.player.ui.pager.PlayerPagerFragmentFactory + +class VideoFileFragmentFactory : PlayerPagerFragmentFactory { + + override fun create(item: PlaybackFile): Fragment { + return VideoFileFragment.createInstance(item) + } +} diff --git a/app/src/main/java/com/ionos/player/ui/video/VideoPlayerView.kt b/app/src/main/java/com/ionos/player/ui/video/VideoPlayerView.kt new file mode 100644 index 000000000000..cb119c6b5e87 --- /dev/null +++ b/app/src/main/java/com/ionos/player/ui/video/VideoPlayerView.kt @@ -0,0 +1,102 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.ui.video + +import android.content.Context +import android.view.MotionEvent +import android.view.WindowInsets +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.ionos.player.ui.PlayerView +import com.owncloud.android.R +import dagger.android.HasAndroidInjector +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class VideoPlayerView(context: Context) : PlayerView(context) { + + companion object { + private const val HIDE_CONTROLS_DELAY = 5000L + } + + override val layoutRes get() = R.layout.player_video_view + + override val fragmentFactory get() = VideoFileFragmentFactory() + + private var hideControlsTimerJob: Job? = null + + override fun inject(context: Context) { + (context.applicationContext as HasAndroidInjector).androidInjector().inject(this) + } + + override fun onStart() { + super.onStart() + showControls() + } + + override fun onStop() { + super.onStop() + cancelHideControlsTimer() + playbackModel.setVideoSurfaceView(null) + } + + override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { + val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowInsets) + val insets = windowInsetsCompat.getInsets(Type.systemBars() or Type.displayCutout()) + + topBar.setPadding(insets.left, insets.top, insets.right, 0) + playerControlView.setPadding(insets.left, 0, insets.right, insets.bottom) + + windowWrapper.setupStatusBar(R.color.player_video_toolbar_background_color, false) + windowWrapper.setupNavigationBar(R.color.player_video_control_view_background_color, false) + + return WindowInsetsCompat.CONSUMED.toWindowInsets() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + val isTouchOutsideControls = event.y < playerControlView.y && event.y > topBar.height + when { + !playerControlView.isVisible -> showControls() + isTouchOutsideControls -> hideControls() + else -> restartHideControlsTimer() + } + } + return super.dispatchTouchEvent(event) + } + + fun showControls() { + windowWrapper.showSystemBars() + topBar.visibility = VISIBLE + playerControlView.visibility = VISIBLE + restartHideControlsTimer() + } + + fun hideControls() { + windowWrapper.hideSystemBars() + topBar.visibility = GONE + playerControlView.visibility = GONE + cancelHideControlsTimer() + } + + private fun restartHideControlsTimer() { + hideControlsTimerJob?.cancel() + hideControlsTimerJob = activity.lifecycleScope.launch { + delay(HIDE_CONTROLS_DELAY) + hideControls() + } + } + + private fun cancelHideControlsTimer() { + hideControlsTimerJob?.cancel() + hideControlsTimerJob = null + } +} diff --git a/app/src/main/java/com/ionos/player/util/Action.java b/app/src/main/java/com/ionos/player/util/Action.java new file mode 100644 index 000000000000..a56ddc591500 --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/Action.java @@ -0,0 +1,12 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util; + +public interface Action { + void execute(); +} diff --git a/app/src/main/java/com/ionos/player/util/ContentResolver.kt b/app/src/main/java/com/ionos/player/util/ContentResolver.kt new file mode 100644 index 000000000000..3366babf3fda --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/ContentResolver.kt @@ -0,0 +1,26 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util + +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow + +fun ContentResolver.observeContentChanges(uri: Uri, notifyForDescendants: Boolean) = callbackFlow { + val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + trySend(selfChange) + } + } + registerContentObserver(uri, notifyForDescendants, contentObserver) + awaitClose { unregisterContentObserver(contentObserver) } +} diff --git a/app/src/main/java/com/ionos/player/util/Context.kt b/app/src/main/java/com/ionos/player/util/Context.kt new file mode 100644 index 000000000000..092619c9f5d6 --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/Context.kt @@ -0,0 +1,38 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Process +import androidx.annotation.ChecksSdkIntAtLeast + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) +fun Context.isPictureInPictureAllowed(): Boolean { + if (isPictureInPictureSupported()) { + val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager + appOpsManager?.let { + val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + it.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) + } else { + @Suppress("DEPRECATION") + it.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) + } + return mode == AppOpsManager.MODE_ALLOWED + } + } + return false +} + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) +private fun Context.isPictureInPictureSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) +} diff --git a/app/src/main/java/com/ionos/player/util/PeriodicAction.java b/app/src/main/java/com/ionos/player/util/PeriodicAction.java new file mode 100644 index 000000000000..794e63afa754 --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/PeriodicAction.java @@ -0,0 +1,39 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util; + +import android.os.Handler; + +public class PeriodicAction { + + private final Handler handler = new Handler(); + private final int periodicIntervalInMilliseconds; + private final Action action; + + public PeriodicAction(int periodicIntervalInMilliseconds, Action action) { + this.periodicIntervalInMilliseconds = periodicIntervalInMilliseconds; + this.action = action; + } + + public void start() { + stop(); + this.handler.postDelayed(this.runnable, this.periodicIntervalInMilliseconds); + } + + public void stop() { + this.handler.removeCallbacks(this.runnable); + } + + private final Runnable runnable = new Runnable() { + @Override + public void run() { + action.execute(); + start(); + } + }; +} diff --git a/app/src/main/java/com/ionos/player/util/ScreenUtils.kt b/app/src/main/java/com/ionos/player/util/ScreenUtils.kt new file mode 100644 index 000000000000..584895396285 --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/ScreenUtils.kt @@ -0,0 +1,47 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +@file:JvmName("ScreenUtils") + +package com.ionos.player.util + +import android.content.Context +import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.annotation.RequiresApi + +fun Context.getDisplayWidth(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + getWindowMetrics().bounds.width() + } else { + getDisplayMetrics().widthPixels + } +} + +fun Context.getDisplayHeight(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + getWindowMetrics().bounds.height() + } else { + getDisplayMetrics().heightPixels + } +} + +@RequiresApi(Build.VERSION_CODES.R) +fun Context.getWindowMetrics(): WindowMetrics { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + return windowManager.currentWindowMetrics +} + +@Suppress("DEPRECATION") +fun Context.getDisplayMetrics(): DisplayMetrics { + val displayMetrics = DisplayMetrics() + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.defaultDisplay.getRealMetrics(displayMetrics) + return displayMetrics +} diff --git a/app/src/main/java/com/ionos/player/util/SystemVersion.java b/app/src/main/java/com/ionos/player/util/SystemVersion.java new file mode 100644 index 000000000000..436de1e3661e --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/SystemVersion.java @@ -0,0 +1,40 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util; + +import android.os.Build; + +import androidx.annotation.ChecksSdkIntAtLeast; + +public class SystemVersion { + + /** + * Checks if run on Android 11 Red Velvet Cake (30) or newer + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + public static boolean greaterOrEqualToR() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; + } + + /** + * Checks if run on Android 12 Snow Cone (31) or newer + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) + public static boolean greaterOrEqualToS() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + } + + /** + * Checks if run on Android 13 Tiramisu (33) or newer + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + public static boolean greaterOrEqualToTiramisu() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; + } + +} diff --git a/app/src/main/java/com/ionos/player/util/WindowWrapper.kt b/app/src/main/java/com/ionos/player/util/WindowWrapper.kt new file mode 100644 index 000000000000..49c47cfb2d6a --- /dev/null +++ b/app/src/main/java/com/ionos/player/util/WindowWrapper.kt @@ -0,0 +1,55 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.player.util + +import android.os.Build +import android.view.Window +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +class WindowWrapper( + private val window: Window, +) { + private val context = window.context + private val insetsController = WindowCompat.getInsetsController(window, window.decorView) + + fun showSystemBars() { + insetsController.show(WindowInsetsCompat.Type.systemBars()) + } + + fun hideSystemBars() { + insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + insetsController.hide(WindowInsetsCompat.Type.systemBars()) + } + + fun setupStatusBar(@ColorRes backgroundColorRes: Int, contrastEnforced: Boolean) { + val backgroundColor = ContextCompat.getColor(context, backgroundColorRes) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.setStatusBarContrastEnforced(contrastEnforced) + } + insetsController.isAppearanceLightStatusBars = isLightColor(backgroundColor) + } + + fun setupNavigationBar(@ColorRes backgroundColorRes: Int, contrastEnforced: Boolean) { + val backgroundColor = ContextCompat.getColor(context, backgroundColorRes) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.setNavigationBarContrastEnforced(contrastEnforced) + } + window.navigationBarColor = backgroundColor + insetsController.isAppearanceLightNavigationBars = isLightColor(backgroundColor) + } + + private fun isLightColor(@ColorInt color: Int): Boolean { + return ColorUtils.calculateLuminance(color) > 0.5 + } +} diff --git a/app/src/main/java/com/ionos/privacy/DataProtectionActivity.kt b/app/src/main/java/com/ionos/privacy/DataProtectionActivity.kt new file mode 100644 index 000000000000..fcd90a1dec87 --- /dev/null +++ b/app/src/main/java/com/ionos/privacy/DataProtectionActivity.kt @@ -0,0 +1,150 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.privacy + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.text.method.LinkMovementMethod +import androidx.activity.SystemBarStyle +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.ionos.annotation.IonosCustomization +import com.ionos.utils.context.isDarkMode +import com.ionos.utils.text.convertAnnotatedTextToLinks +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityDataProtectionBinding +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.ui.activity.ExternalSiteWebView +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class DataProtectionActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: DataProtectionViewModel.Factory + + private val viewModel by viewModels { viewModelFactory } + + private val binding by lazy { ActivityDataProtectionBinding.inflate(layoutInflater) } + + private val detailPageOnBackPressedCallback by lazy { + onBackPressedDispatcher.addCallback(this) { viewModel.onDetailPageBackButtonClick() } + } + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge(SystemBarStyle.dark(Color.TRANSPARENT), SystemBarStyle.dark(Color.TRANSPARENT)) + super.onCreate(savedInstanceState) + setContentView(binding.root) + + val descriptionText = getText(R.string.ionos_data_protection_description).convertAnnotatedTextToLinks( + linkColor = ContextCompat.getColor(this, R.color.curious_blue), + linkUnderline = false, + linkHandler = ::handleLink, + ) + + binding.overviewPage.descriptionTextView.text = descriptionText + binding.overviewPage.descriptionTextView.movementMethod = LinkMovementMethod.getInstance() + + binding.overviewPage.agreeButton.setOnClickListener { viewModel.onAgreeButtonClick() } + binding.overviewPage.settingsButton.setOnClickListener { viewModel.onSettingsButtonClick() } + binding.detailPage.toolbar.setNavigationOnClickListener { viewModel.onDetailPageBackButtonClick() } + binding.detailPage.saveButton.setOnClickListener { viewModel.onSaveButtonClick() } + binding.detailPage.switchers.analyticsSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.onAnalyticsCheckedChange(isChecked) + } + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val insets = windowInsets.getInsets(insetsType) + binding.overviewPage.root.setPadding(insets.left, insets.top, insets.right, insets.bottom) + binding.detailPage.root.setPadding(insets.left, insets.top, insets.right, insets.bottom) + WindowInsetsCompat.CONSUMED + } + + viewModel.stateFlow + .flowWithLifecycle(lifecycle) + .onEach(::updateState) + .launchIn(lifecycleScope) + } + + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + + private fun handleLink(type: String) { + when (type) { + INFORMATION_LINK -> openPrivacyPolicyScreen() + REJECT_LINK -> viewModel.onRejectLinkClick() + else -> throw IllegalArgumentException("Unknown link type: $type") + } + } + + private fun openPrivacyPolicyScreen() { + val externalWebViewIntent = Intent(this, ExternalSiteWebView::class.java) + externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, getString(R.string.privacy)) + externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_URL, getString(R.string.privacy_url)) + externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + startActivity(externalWebViewIntent) + } + + private fun updateState(state: DataProtectionViewModel.State) { + if (binding.viewSwitcher.displayedChild != state.page.index) { + binding.viewSwitcher.displayedChild = state.page.index + } + if (state.page == DataProtectionViewModel.Page.OVERVIEW) { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + setSystemBarsAppearance(false) + detailPageOnBackPressedCallback.isEnabled = false + } else { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_UNSPECIFIED + setSystemBarsAppearance(!isDarkMode()) + detailPageOnBackPressedCallback.isEnabled = true + } + if (binding.detailPage.switchers.analyticsSwitch.isChecked != state.isAnalyticsEnabled) { + binding.detailPage.switchers.analyticsSwitch.isChecked = state.isAnalyticsEnabled + } + if (state.isProcessed) { + intent.getParcelableArgument(TARGET_SCREEN_INTENT_KEY, Intent::class.java)?.let(::startActivity) + finish() + } + } + + private fun setSystemBarsAppearance(isLight: Boolean) { + val controller = WindowInsetsControllerCompat(window, window.decorView) + controller.isAppearanceLightStatusBars = isLight + controller.isAppearanceLightNavigationBars = isLight + } + + companion object { + private const val TARGET_SCREEN_INTENT_KEY = "target_screen_intent" + private const val INFORMATION_LINK = "information_link" + private const val REJECT_LINK = "reject_link" + + @JvmStatic + fun createIntent(context: Context): Intent { + return Intent(context, DataProtectionActivity::class.java) + } + + @JvmStatic + fun createIntent(context: Context, targetScreenIntent: Intent): Intent { + return Intent(context, DataProtectionActivity::class.java) + .putExtra(TARGET_SCREEN_INTENT_KEY, targetScreenIntent) + } + } +} diff --git a/app/src/main/java/com/ionos/privacy/DataProtectionViewModel.kt b/app/src/main/java/com/ionos/privacy/DataProtectionViewModel.kt new file mode 100644 index 000000000000..42cdb92c1e99 --- /dev/null +++ b/app/src/main/java/com/ionos/privacy/DataProtectionViewModel.kt @@ -0,0 +1,81 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.privacy + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.ionos.analytics.AnalyticsManager +import com.nextcloud.client.account.UserAccountManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Provider + +class DataProtectionViewModel @Inject constructor( + private val analyticsManager: AnalyticsManager, + private val privacyPreferences: PrivacyPreferences, + private val userAccountManager: UserAccountManager, +) : ViewModel() { + + private var _stateFlow = MutableStateFlow(createInitialState()) + val stateFlow = _stateFlow.asStateFlow() + + fun onAgreeButtonClick() { + save(isAnalyticsEnabled = true) + } + + fun onRejectLinkClick() { + save(isAnalyticsEnabled = false) + } + + fun onSettingsButtonClick() { + _stateFlow.update { it.copy(page = Page.DETAIL) } + } + + fun onDetailPageBackButtonClick() { + _stateFlow.update { it.copy(page = Page.OVERVIEW) } + } + + fun onAnalyticsCheckedChange(isChecked: Boolean) { + _stateFlow.update { it.copy(isAnalyticsEnabled = isChecked) } + } + + fun onSaveButtonClick() { + save(stateFlow.value.isAnalyticsEnabled) + } + + private fun save(isAnalyticsEnabled: Boolean) { + analyticsManager.setEnabled(isAnalyticsEnabled) + privacyPreferences.setAnalyticsEnabled(isAnalyticsEnabled) + privacyPreferences.setDataProtectionProcessed(userAccountManager.currentOwnCloudAccount?.name) + _stateFlow.update { it.copy(isProcessed = true) } + } + + enum class Page(val index: Int) { + OVERVIEW(0), + DETAIL(1), + } + + private fun createInitialState(): State = State(isAnalyticsEnabled = privacyPreferences.isAnalyticsEnabled()) + + data class State( + val page: Page = Page.OVERVIEW, + val isAnalyticsEnabled: Boolean = false, + val isProcessed: Boolean = false, + ) + + class Factory @Inject constructor( + private val viewModelProvider: Provider, + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return viewModelProvider.get() as T + } + } +} diff --git a/app/src/main/java/com/ionos/privacy/PrivacyPreferences.kt b/app/src/main/java/com/ionos/privacy/PrivacyPreferences.kt new file mode 100644 index 000000000000..1dc35f7c7567 --- /dev/null +++ b/app/src/main/java/com/ionos/privacy/PrivacyPreferences.kt @@ -0,0 +1,59 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.privacy + +import android.content.Context +import javax.inject.Inject + +class PrivacyPreferences @Inject constructor( + private val context: Context, +) { + private val sharedPreferences by lazy { context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) } + + fun isDataProtectionProcessed(accountName: String?): Boolean = + getAccountsWithProcessedDataProtection().contains(accountName) + + fun setDataProtectionProcessed(accountName: String?) { + accountName?.let { + sharedPreferences + .edit() + .putStringSet(DATA_PROTECTION_PROCESSED_KEY, getAccountsWithProcessedDataProtection() + accountName) + .apply() + } + } + + fun removeDataProtectionProcessed(accountName: String) { + mutableSetOf(*getAccountsWithProcessedDataProtection().toTypedArray()) + .apply { remove(accountName) } + .let { accountsWithProcessedDataProtection -> + sharedPreferences + .edit() + .putStringSet(DATA_PROTECTION_PROCESSED_KEY, accountsWithProcessedDataProtection) + .apply() + } + } + + private fun getAccountsWithProcessedDataProtection(): Set = + sharedPreferences + .getStringSet(DATA_PROTECTION_PROCESSED_KEY, emptySet()) + ?: emptySet() + + fun isAnalyticsEnabled(): Boolean { + return sharedPreferences.getBoolean(ANALYTICS_ENABLED_KEY, false) + } + + fun setAnalyticsEnabled(value: Boolean) { + sharedPreferences.edit().putBoolean(ANALYTICS_ENABLED_KEY, value).apply() + } + + companion object { + private const val FILE_NAME = "privacy_preferences" + private const val DATA_PROTECTION_PROCESSED_KEY = "data_protection_processed" + private const val ANALYTICS_ENABLED_KEY = "analytics_enabled" + } +} diff --git a/app/src/main/java/com/ionos/privacy/PrivacySettingsActivity.kt b/app/src/main/java/com/ionos/privacy/PrivacySettingsActivity.kt new file mode 100644 index 000000000000..e867ffabfd78 --- /dev/null +++ b/app/src/main/java/com/ionos/privacy/PrivacySettingsActivity.kt @@ -0,0 +1,83 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.privacy + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.ionos.annotation.IonosCustomization +import com.owncloud.android.databinding.ActivityPrivacySettingsBinding +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class PrivacySettingsActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: PrivacySettingsViewModel.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private val viewModel by viewModels { viewModelFactory } + + private val binding by lazy { ActivityPrivacySettingsBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + viewThemeUtils.platform.themeStatusBar(this); + + binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + binding.switchers.analyticsSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.onAnalyticsCheckedChange(isChecked) + } + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val insets = windowInsets.getInsets(insetsType) + binding.root.setPadding(insets.left, insets.top, insets.right, insets.bottom) + WindowInsetsCompat.CONSUMED + } + + viewModel.stateFlow + .flowWithLifecycle(lifecycle) + .onEach(::updateState) + .launchIn(lifecycleScope) + } + + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + + override fun onStart() { + super.onStart() + viewModel.onStart() + } + + private fun updateState(state: PrivacySettingsViewModel.State) { + if (binding.switchers.analyticsSwitch.isChecked != state.isAnalyticsEnabled) { + binding.switchers.analyticsSwitch.isChecked = state.isAnalyticsEnabled + } + } + + companion object { + + @JvmStatic + fun createIntent(context: Context): Intent { + return Intent(context, PrivacySettingsActivity::class.java) + } + } +} diff --git a/app/src/main/java/com/ionos/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/com/ionos/privacy/PrivacySettingsViewModel.kt new file mode 100644 index 000000000000..d65d9adf6ca6 --- /dev/null +++ b/app/src/main/java/com/ionos/privacy/PrivacySettingsViewModel.kt @@ -0,0 +1,50 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.privacy + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.ionos.analytics.AnalyticsManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Provider + +class PrivacySettingsViewModel @Inject constructor( + private val analyticsManager: AnalyticsManager, + private val privacyPreferences: PrivacyPreferences, +) : ViewModel() { + + private var _stateFlow = MutableStateFlow(State()) + val stateFlow = _stateFlow.asStateFlow() + + fun onStart() { + val isAnalyticsEnabled = privacyPreferences.isAnalyticsEnabled() + _stateFlow.update { it.copy(isAnalyticsEnabled = isAnalyticsEnabled) } + } + + fun onAnalyticsCheckedChange(isChecked: Boolean) { + _stateFlow.update { it.copy(isAnalyticsEnabled = isChecked) } + analyticsManager.setEnabled(isChecked) + privacyPreferences.setAnalyticsEnabled(isChecked) + } + + data class State( + val isAnalyticsEnabled: Boolean = false, + ) + + class Factory @Inject constructor( + private val viewModelProvider: Provider, + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return viewModelProvider.get() as T + } + } +} diff --git a/app/src/main/java/com/ionos/scanbot/di/NCScanbotModule.kt b/app/src/main/java/com/ionos/scanbot/di/NCScanbotModule.kt new file mode 100644 index 000000000000..9bd8d3cafe6d --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/di/NCScanbotModule.kt @@ -0,0 +1,46 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.di + +import com.ionos.scanbot.image_loader.ImageLoader +import com.ionos.scanbot.image_loading.ImageLoaderImpl +import com.ionos.scanbot.license.LoadScanbotLicense +import com.ionos.scanbot.license.LoadScanbotLicenseImpl +import com.ionos.scanbot.logger.ScanbotLogger +import com.ionos.scanbot.screens.save.SelectDirectoryContract +import com.ionos.scanbot.upload.ScanbotUploader +import com.ionos.scanbot.upload.SelectDirectoryContractImpl +import com.ionos.scanbot.upload.use_case.Uploader +import com.ionos.scanbot.util.GetLocalFreeSpace +import com.ionos.scanbot.util.logger.Logger +import com.ionos.scanbot.utils.GetLocalFreeSpaceImpl +import dagger.Binds +import dagger.Module + +@Module(includes = [ScanbotModule::class]) +abstract class NCScanbotModule { + + @Binds + abstract fun bindLoadLicense(loadScanbotLicense: LoadScanbotLicenseImpl): LoadScanbotLicense + + @Binds + abstract fun bindImageLoader(imageLoader: ImageLoaderImpl): ImageLoader + + @Binds + abstract fun bindUploader(uploader: ScanbotUploader): Uploader + + @Binds + abstract fun bindLogger(logger: ScanbotLogger): Logger + + @Binds + abstract fun bindSelectDirectoryContract(contract: SelectDirectoryContractImpl): SelectDirectoryContract + + @Binds + abstract fun bindGetLocalFreeSpaceImpl(getLocalFreeSpaceImpl: GetLocalFreeSpaceImpl): GetLocalFreeSpace + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/image_loading/ImageLoaderImpl.kt b/app/src/main/java/com/ionos/scanbot/image_loading/ImageLoaderImpl.kt new file mode 100644 index 000000000000..59a932a196b4 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/image_loading/ImageLoaderImpl.kt @@ -0,0 +1,22 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.image_loading + +import com.ionos.scanbot.image_loader.ImageLoader +import com.ionos.scanbot.image_loader.ImageRequestBuilder +import java.io.File +import javax.inject.Inject + +class ImageLoaderImpl @Inject constructor( +) : ImageLoader { + + override fun load(file: File): ImageRequestBuilder { + return ImageRequestBuilderImpl(file) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/image_loading/ImageRequestBuilderImpl.kt b/app/src/main/java/com/ionos/scanbot/image_loading/ImageRequestBuilderImpl.kt new file mode 100644 index 000000000000..ee2e2f3e909a --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/image_loading/ImageRequestBuilderImpl.kt @@ -0,0 +1,42 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.image_loading + +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.signature.ObjectKey +import com.ionos.scanbot.image_loader.ImageLoaderOptions +import com.ionos.scanbot.image_loader.ImageRequestBuilder +import com.ionos.scanbot.image_loader.ScaleType +import java.io.File + +class ImageRequestBuilderImpl( + private val file: File, +) : ImageRequestBuilder { + + private var options: ImageLoaderOptions? = null + + override fun options(options: ImageLoaderOptions): ImageRequestBuilder { + this.options = options + return this + } + + override fun into(target: ImageView) { + val baseRequest = Glide.with(target.context).load(file) + + val request = when (options?.scaleType) { + ScaleType.CENTER_CROP -> baseRequest.centerCrop() + ScaleType.CENTER_INSIDE -> baseRequest.fitCenter() + null -> baseRequest + } + + request + .signature(ObjectKey(file.lastModified())) + .into(target) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/image_loading/ObjectKey.java b/app/src/main/java/com/ionos/scanbot/image_loading/ObjectKey.java new file mode 100644 index 000000000000..de9b1fec7535 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/image_loading/ObjectKey.java @@ -0,0 +1,49 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.image_loading; + +import com.bumptech.glide.load.Key; + +import java.nio.charset.Charset; +import java.security.MessageDigest; + +import androidx.annotation.NonNull; + +public final class ObjectKey implements Key { + private static final Charset CHARSET = Charset.forName(STRING_CHARSET_NAME); + + private final Object object; + + public ObjectKey(@NonNull Object object) { + this.object = object; + } + + @Override + public String toString() { + return "ObjectKey{" + "object=" + object + '}'; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ObjectKey) { + ObjectKey other = (ObjectKey) o; + return object.equals(other.object); + } + return false; + } + + @Override + public int hashCode() { + return object.hashCode(); + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(object.toString().getBytes(CHARSET)); + } +} diff --git a/app/src/main/java/com/ionos/scanbot/license/DownloadLicenseRemoteOperation.kt b/app/src/main/java/com/ionos/scanbot/license/DownloadLicenseRemoteOperation.kt new file mode 100644 index 000000000000..3f7e8dc70476 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/license/DownloadLicenseRemoteOperation.kt @@ -0,0 +1,59 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.license + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import org.apache.commons.httpclient.methods.GetMethod + +class DownloadLicenseRemoteOperation( + private val licenseKeyUrl: String, +) : RemoteOperation() { + + var license: String? = null + private set + + override fun run(client: OwnCloudClient?): RemoteOperationResult { + return try { + if (client == null) throw IllegalArgumentException("Client should not be null") + + val getMethod = GetMethod(licenseKeyUrl) + + license = downloadFile(client, getMethod) + RemoteOperationResult(true, getMethod) + } catch (e: Exception) { + RemoteOperationResult(e) + } + } + + private fun downloadFile( + client: OwnCloudClient, + getMethod: GetMethod, + ): String? { + try { + val status = client.executeMethod(getMethod) + if (isSuccess(status)) { + val bytes = getMethod.responseBodyAsStream + .use { + it.readBytes() + } + + return LicenseResponseTransformer().transform(bytes) + } + } finally { + getMethod.releaseConnection() + } + + return null + } + + private fun isSuccess(status: Int): Boolean { + return status == 200 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/license/LicenseResponseTransformer.kt b/app/src/main/java/com/ionos/scanbot/license/LicenseResponseTransformer.kt new file mode 100644 index 000000000000..92ebbce1a2c6 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/license/LicenseResponseTransformer.kt @@ -0,0 +1,39 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.license + +internal class LicenseResponseTransformer { + + companion object { + private const val SEPARATOR = "\"" + private const val ESCAPED_NEW_LINE = "\\n" + private const val NEW_LINE = "\n" + } + + fun transform(bytes: ByteArray): String? { + val inlinedResponse = String(bytes) + + var scanbotLicenseKey = "" + var startIndex = inlinedResponse.indexOf(SEPARATOR) + while (startIndex != -1) { + val endIndex = inlinedResponse.indexOf(SEPARATOR, startIndex + 1) + if (endIndex == -1) { + scanbotLicenseKey = "" + break + } + + scanbotLicenseKey += inlinedResponse.substring(startIndex + 1, endIndex) + + startIndex = inlinedResponse.indexOf(SEPARATOR, endIndex + 1) + } + + scanbotLicenseKey = scanbotLicenseKey.replace(ESCAPED_NEW_LINE, NEW_LINE) + + return if (scanbotLicenseKey.isNotBlank()) scanbotLicenseKey else null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/license/LoadScanbotLicenseImpl.kt b/app/src/main/java/com/ionos/scanbot/license/LoadScanbotLicenseImpl.kt new file mode 100644 index 000000000000..f36783fe41c9 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/license/LoadScanbotLicenseImpl.kt @@ -0,0 +1,42 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.license + +import android.content.Context +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import javax.inject.Inject + +class LoadScanbotLicenseImpl @Inject constructor( + context: Context, +) : LoadScanbotLicense { + + private val workManager by lazy { WorkManager.getInstance(context) } + + override fun invoke() { + val request = + OneTimeWorkRequest.Builder(ScanbotLicenseDownloadWorker::class.java) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + + workManager.enqueueUniqueWork( + ScanbotLicenseDownloadWorker.SCANBOT_LICENSE_DOWNLOAD_WORKER, + ExistingWorkPolicy.KEEP, + request + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseDownloadWorker.kt b/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseDownloadWorker.kt new file mode 100644 index 000000000000..ca85c02f274c --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseDownloadWorker.kt @@ -0,0 +1,86 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.license + +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.work.ForegroundInfo +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.ionos.scanbot.initializer.TryToInitScanbotSdk +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.download.DownloadNotificationManager +import com.owncloud.android.R +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.security.SecureRandom + +class ScanbotLicenseDownloadWorker( + licenseUrl: String, + viewThemeUtils: ViewThemeUtils, + private val accountManager: UserAccountManager, + private val licenseKeyStore: LicenseKeyStore, + private val tryToInitScanbotSdk: TryToInitScanbotSdk, + private val context: Context, + params: WorkerParameters, +): Worker(context, params){ + + companion object { + const val SCANBOT_LICENSE_DOWNLOAD_WORKER = "SCANBOT_LICENSE_DOWNLOAD_WORKER" + } + + private val operation = DownloadLicenseRemoteOperation(licenseUrl) + private var notificationManager = DownloadNotificationManager( + SecureRandom().nextInt(), + context, + viewThemeUtils + ) + + override fun doWork(): Result { + return try { + val ocAccount = accountManager.user.toOwnCloudAccount() + val downloadClient = + OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context) + + val result = operation.execute(downloadClient) + + if (result.isSuccess) { + operation.license?.let { + licenseKeyStore.saveLicenseKey(it) + tryToInitScanbotSdk(it) + } + Result.success() + } + else Result.retry() + }catch (e: Exception){ + Result.failure() + } + } + + override fun getForegroundInfo(): ForegroundInfo { + notificationManager.notificationBuilder.run { + setProgress(100, 0, true) + setContentTitle(context.getString(R.string.downloader_download_in_progress_ticker)) + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ + ForegroundInfo( + notificationManager.getId(), + notificationManager.getNotification(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else{ + ForegroundInfo( + notificationManager.getId(), + notificationManager.getNotification(), + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseJobFactory.kt b/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseJobFactory.kt new file mode 100644 index 000000000000..b5ddc52b7892 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/license/ScanbotLicenseJobFactory.kt @@ -0,0 +1,39 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.license + +import android.content.Context +import androidx.work.WorkerParameters +import com.ionos.scanbot.di.qualifiers.ScanbotLicenseKeyUrl +import com.ionos.scanbot.initializer.TryToInitScanbotSdk +import com.nextcloud.client.account.UserAccountManager +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject +import javax.inject.Provider + +class ScanbotLicenseJobFactory @Inject constructor( + @ScanbotLicenseKeyUrl private val scanbotLicenseUrl: String, + private val accountManager: UserAccountManager, + private val licenseKeyStore: LicenseKeyStore, + private val tryToInitScanbotSdk: TryToInitScanbotSdk, + private val viewThemeUtils: Provider, +) { + + fun create(context: Context, params: WorkerParameters): ScanbotLicenseDownloadWorker { + return ScanbotLicenseDownloadWorker( + scanbotLicenseUrl, + viewThemeUtils.get(), + accountManager, + licenseKeyStore, + tryToInitScanbotSdk, + context, + params + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/logger/ScanbotLogger.kt b/app/src/main/java/com/ionos/scanbot/logger/ScanbotLogger.kt new file mode 100644 index 000000000000..46c7a67f2de4 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/logger/ScanbotLogger.kt @@ -0,0 +1,28 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.logger + +import com.ionos.scanbot.util.logger.Logger +import javax.inject.Inject +import com.nextcloud.client.logger.Logger as NextCloudLogger + +class ScanbotLogger @Inject constructor( + private val nextCloudLogger: NextCloudLogger +) : Logger { + + companion object { + private const val GLOBAL_TAG = "ScanbotModule" + } + + override fun logE(message: String, t: Throwable?) { + t?.let { + nextCloudLogger.e(GLOBAL_TAG, message, it) + } ?: nextCloudLogger.e(GLOBAL_TAG, message) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/ui/ScanbotCameraPermissionHandler.kt b/app/src/main/java/com/ionos/scanbot/ui/ScanbotCameraPermissionHandler.kt new file mode 100644 index 000000000000..7e81456a0504 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/ui/ScanbotCameraPermissionHandler.kt @@ -0,0 +1,60 @@ +package com.ionos.scanbot.ui + +import android.view.View +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.ionos.scanbot.controller.ScanbotController +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.DisplayUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable + +class ScanbotCameraPermissionHandler( + private val scanbotController: ScanbotController, + private val viewProvider: ViewProvider +) : DefaultLifecycleObserver { + + private var disposables: CompositeDisposable? = null + + companion object { + private val TAG = ScanbotCameraPermissionHandler::class.java.simpleName + } + + fun interface ViewProvider { + fun getSnackbarView(): View? + } + + override fun onCreate(owner: LifecycleOwner) { + disposables = CompositeDisposable() + subscribeToScanbotEvents() + } + + override fun onDestroy(owner: LifecycleOwner) { + disposables?.clear() + disposables = null + owner.lifecycle.removeObserver(this) + } + + private fun subscribeToScanbotEvents() { + disposables?.add( + scanbotController.cameraPermissionDenied + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { showCameraPermissionDeniedMessage() }, + { error -> Log_OC.e(TAG, "Error in camera permission stream", error) } + ) + ) + } + + private fun showCameraPermissionDeniedMessage() { + val view = viewProvider.getSnackbarView() ?: return + val context = view.context + + DisplayUtils.showSnackMessageWithSettingsAction( + view, + context.getString(R.string.ionos_need_camera_permissions), + context + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/upload/ScanbotUploader.kt b/app/src/main/java/com/ionos/scanbot/upload/ScanbotUploader.kt new file mode 100644 index 000000000000..6ddebd69a5a1 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/upload/ScanbotUploader.kt @@ -0,0 +1,39 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.upload + +import com.ionos.scanbot.upload.use_case.Uploader +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.operations.UploadFileOperation +import java.io.File +import javax.inject.Inject + +class ScanbotUploader @Inject constructor( + private val currentAccountProvider: CurrentAccountProvider +): Uploader { + override fun upload(uploadFolder: String, pageList: List) { + val uploadPaths = pageList.map { + File(uploadFolder, File(it).name).path + }.toTypedArray() + + FileUploadHelper.instance().uploadNewFiles( + currentAccountProvider.user, + pageList.toTypedArray(), + uploadPaths, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + true, + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.RENAME + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/upload/SelectDirectoryContractImpl.kt b/app/src/main/java/com/ionos/scanbot/upload/SelectDirectoryContractImpl.kt new file mode 100644 index 000000000000..9ea88c830d98 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/upload/SelectDirectoryContractImpl.kt @@ -0,0 +1,42 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.upload + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.ionos.scanbot.screens.save.SelectDirectoryContract +import com.ionos.scanbot.upload.target_provider.ScanbotUploadTarget +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FolderPickerActivity +import javax.inject.Inject + +class SelectDirectoryContractImpl @Inject constructor( +) : SelectDirectoryContract() { + + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(context, FolderPickerActivity::class.java) + .apply { + putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): SelectDirectoryResult { + if (resultCode != Activity.RESULT_OK || intent == null || FolderPickerActivity.EXTRA_FOLDER == null) + return SelectDirectoryResult.Canceled + + return intent.getParcelableArgument(FolderPickerActivity.EXTRA_FOLDER, OCFile::class.java) + ?.remotePath?.let { + SelectDirectoryResult.Success( + ScanbotUploadTarget(it) + ) + } ?: SelectDirectoryResult.Canceled + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/scanbot/utils/GetLocalFreeSpaceImpl.kt b/app/src/main/java/com/ionos/scanbot/utils/GetLocalFreeSpaceImpl.kt new file mode 100644 index 000000000000..e5537e9859b0 --- /dev/null +++ b/app/src/main/java/com/ionos/scanbot/utils/GetLocalFreeSpaceImpl.kt @@ -0,0 +1,16 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.scanbot.utils + +import com.ionos.scanbot.util.GetLocalFreeSpace +import com.owncloud.android.utils.FileStorageUtils +import javax.inject.Inject + +class GetLocalFreeSpaceImpl @Inject constructor() : GetLocalFreeSpace { + override fun invoke(): Long = FileStorageUtils.getUsableSpace() +} \ No newline at end of file diff --git a/app/src/main/java/com/ionos/startup/IonosInitializer.kt b/app/src/main/java/com/ionos/startup/IonosInitializer.kt new file mode 100644 index 000000000000..cb24098ee3dc --- /dev/null +++ b/app/src/main/java/com/ionos/startup/IonosInitializer.kt @@ -0,0 +1,43 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH.. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.startup + +import android.content.Context +import androidx.startup.Initializer +import com.ionos.analytics.AnalyticsManager +import com.ionos.privacy.PrivacyPreferences +import com.ionos.scanbot.initializer.ScanbotInitializer +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.MainApp +import javax.inject.Inject + +class IonosInitializer : Initializer { + + @Inject + lateinit var analyticsManager: AnalyticsManager + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var privacyPreferences: PrivacyPreferences + + @Inject + lateinit var scanbotInitializer: ScanbotInitializer + + override fun create(context: Context) { + (context.applicationContext as MainApp).androidInjector().inject(this) + analyticsManager.setEnabled(privacyPreferences.isAnalyticsEnabled()) + appPreferences.setShowHiddenFilesEnabled(true); + scanbotInitializer.initialize() + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/java/com/ionos/utils/IonosAndroidViewThemeUtils.kt b/app/src/main/java/com/ionos/utils/IonosAndroidViewThemeUtils.kt new file mode 100644 index 000000000000..d7d505c6f8aa --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosAndroidViewThemeUtils.kt @@ -0,0 +1,220 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.utils + +import android.app.Activity +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.view.MenuItem +import android.view.View +import android.widget.Button +import android.widget.CheckBox +import android.widget.CheckedTextView +import android.widget.EditText +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.RadioButton +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.appcompat.app.ActionBarDrawerToggle +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationView +import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.common.ui.util.buildColorStateList +import com.owncloud.android.R +import com.owncloud.android.ui.activity.DrawerActivity +import dynamiccolor.DynamicScheme + +class IonosAndroidViewThemeUtils( + private val delegate: AndroidViewThemeUtils, +) { + fun themeStatusBar(activity: Activity) { + delegate.colorStatusBar(activity, activity.getSystemBarsColor()) + } + + fun themeStatusBar(activity: Activity, @ColorInt color: Int) { + delegate.colorStatusBar(activity, color) + } + + fun resetStatusBar(activity: Activity) { + delegate.colorStatusBar(activity, activity.getSystemBarsColor()) + } + + @ColorInt + private fun Context.getSystemBarsColor(): Int = getColor(R.color.system_bars_color) + + @JvmOverloads + fun colorViewBackground(view: View, colorRole: ColorRole = ColorRole.SURFACE) { + if (colorRole == ColorRole.SURFACE) { + // do nothing + } else { + delegate.colorViewBackground(view, colorRole) + } + } + + fun themeDialog(view: View) { + // do nothing + } + + fun colorTextButtons(vararg buttons: Button) { + // do nothing + } + + fun colorCircularProgressBar(progressBar: ProgressBar, colorRole: ColorRole) { + delegate.colorCircularProgressBar(progressBar, colorRole) + } + + fun colorDrawable(drawable: Drawable, @ColorInt color: Int): Drawable { + return delegate.colorDrawable(drawable, color) + } + + fun colorEditText(editText: EditText) { + delegate.colorEditText(editText) + } + + fun colorEditTextOnPrimary(editText: EditText) { + delegate.colorEditTextOnPrimary(editText) + } + + fun colorImageView(imageView: ImageView) { + delegate.colorImageView(imageView) + } + + fun colorImageView(imageView: ImageView, colorRole: ColorRole) { + //delegate.colorImageView(imageView, colorRole) + } + + fun colorImageViewBackgroundAndIcon(imageView: ImageView) { + delegate.colorImageViewBackgroundAndIcon(imageView) + } + + fun colorMenuItemText(context: Context, item: MenuItem) { + delegate.colorMenuItemText(context, item) + } + + fun colorOnSecondaryContainerTextViewElement(textView: TextView) { + delegate.colorOnSecondaryContainerTextViewElement(textView) + } + + fun colorPrimaryTextViewElement(textView: TextView) { + delegate.colorPrimaryTextViewElement(textView) + } + + fun colorStatusBar(activity: Activity, @ColorInt color: Int) { + delegate.colorStatusBar(activity, color) + } + + fun colorSwitch(switch: Switch) { + delegate.colorSwitch(switch) + } + + fun colorTextButtons(@ColorInt color: Int, vararg buttons: Button) { + delegate.colorTextButtons(color, *buttons) + } + + @JvmOverloads + fun colorTextView(textView: TextView, colorRole: ColorRole = ColorRole.PRIMARY) { + delegate.colorTextView(textView, colorRole) + } + + fun colorToolbarMenuIcon(context: Context, item: MenuItem) { + delegate.colorToolbarMenuIcon(context, item) + } + + fun getPrimaryColorDrawable(context: Context): Drawable { + return delegate.getPrimaryColorDrawable(context) + } + + fun primaryColor(activity: Activity): Int { + return delegate.primaryColor(activity) + } + + fun themeCheckbox(vararg checkboxes: CheckBox) { + delegate.themeCheckbox(*checkboxes) + } + + fun themeCheckedTextView(vararg checkedTextViews: CheckedTextView) { + delegate.themeCheckedTextView(*checkedTextViews) + } + + fun themeDialogDivider(view: View) { + delegate.themeDialogDivider(view) + } + + fun themeHorizontalProgressBar(progressBar: ProgressBar) { + delegate.themeHorizontalProgressBar(progressBar) + } + + fun themeRadioButton(radioButton: RadioButton) { + //delegate.themeRadioButton(radioButton) + } + + fun themeStatusBar(activity: Activity, colorRole: ColorRole) { + delegate.themeStatusBar(activity, colorRole) + } + + @JvmOverloads + fun tintDrawable(context: Context, drawable: Drawable, colorRole: ColorRole = ColorRole.PRIMARY): Drawable { + return delegate.tintDrawable(context, drawable, colorRole) + } + + @JvmOverloads + fun tintDrawable(context: Context, @DrawableRes id: Int, colorRole: ColorRole = ColorRole.PRIMARY): Drawable? { + return delegate.tintDrawable(context, id, colorRole) + } + + fun tintPrimaryDrawable(context: Context, drawable: Drawable?): Drawable? { + return delegate.tintPrimaryDrawable(context, drawable) + } + + fun tintPrimaryDrawable(context: Context, @DrawableRes id: Int): Drawable? { + return delegate.tintPrimaryDrawable(context, id) + } + + fun tintTextDrawable(context: Context, drawable: Drawable?): Drawable? { + return delegate.tintTextDrawable(context, drawable) + } + + fun colorBottomNavigationView(bottomNavigationView: BottomNavigationView) { + with(bottomNavigationView) { + setBackgroundResource(R.color.ionos_bottom_navigation_background_color) + + val iconColorInt = context.getColor(R.color.ionos_bottom_navigation_icon_color) + itemIconTintList = ColorStateList.valueOf(iconColorInt) + + val activeIndicatorColorInt = context.getColor(R.color.ionos_bottom_navigation_active_indicator_color) + itemActiveIndicatorColor = ColorStateList.valueOf(activeIndicatorColorInt) + + val rippleColorInt = context.getColor(R.color.ionos_bottom_navigation_ripple_color) + itemRippleColor = ColorStateList.valueOf(rippleColorInt) + + itemTextColor = buildColorStateList( + android.R.attr.state_checked to context.getColor(R.color.text_color), + -android.R.attr.state_checked to context.getColor(R.color.secondary_text_color), + ) + } + } + + fun colorNavigationView(navigationView: NavigationView) { + delegate.colorNavigationView(navigationView) + } + + fun tintToolbarArrowDrawable( + context: Context, + mDrawerToggle: androidx.appcompat.app.ActionBarDrawerToggle, + backArrow: android.graphics.drawable.Drawable + ) { + delegate.tintToolbarArrowDrawable(context, mDrawerToggle, backArrow) + } + + fun getScheme(context: Context): DynamicScheme = delegate.getScheme(context) +} diff --git a/app/src/main/java/com/ionos/utils/IonosAndroidXViewThemeUtils.kt b/app/src/main/java/com/ionos/utils/IonosAndroidXViewThemeUtils.kt new file mode 100644 index 000000000000..e29e45ea887d --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosAndroidXViewThemeUtils.kt @@ -0,0 +1,52 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH.. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.utils + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.appcompat.app.ActionBar +import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SwitchCompat +import androidx.core.app.NotificationCompat +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils + +class IonosAndroidXViewThemeUtils( + private val delegate: AndroidXViewThemeUtils, +) { + + fun colorPrimaryTextViewElement(textView: AppCompatTextView) { + delegate.colorPrimaryTextViewElement(textView) + } + + fun colorSwitchCompat(switchCompat: SwitchCompat) { + //delegate.colorSwitchCompat(switchCompat) + } + + fun themeActionBar(context: Context, actionBar: ActionBar, title: String, backArrow: Drawable) { + actionBar.setHomeAsUpIndicator(backArrow) + actionBar.title = title + } + + fun themeActionBarSubtitle(context: Context, actionBar: ActionBar) { + // do nothing + } + + fun themeNotificationCompatBuilder(context: Context, builder: NotificationCompat.Builder) { + delegate.themeNotificationCompatBuilder(context, builder) + } + + fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) { + delegate.themeSwipeRefreshLayout(swipeRefreshLayout) + } + + fun themeToolbarSearchView(searchView: SearchView) { + // do nothing + } +} diff --git a/app/src/main/java/com/ionos/utils/IonosBuildHelper.kt b/app/src/main/java/com/ionos/utils/IonosBuildHelper.kt new file mode 100644 index 000000000000..b0993e0b25e2 --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosBuildHelper.kt @@ -0,0 +1,27 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.utils + +import com.owncloud.android.BuildConfig + +object IonosBuildHelper { + + private const val IONOS_APPLICATION_ID = "com.ionos.hidrivenext" + private const val HIDRIVE_NEXT_APPLICATION_ID = "com.strato.hidrivenext" + + private val BRANDED_APPLICATION_IDS = setOf( + IONOS_APPLICATION_ID, + HIDRIVE_NEXT_APPLICATION_ID, + ) + + /** True for every white-label HiDrive Next brand, not only the IONOS one. */ + @JvmStatic + fun isIonosBuild(): Boolean { + return BuildConfig.APPLICATION_ID in BRANDED_APPLICATION_IDS + } +} diff --git a/app/src/main/java/com/ionos/utils/IonosDialogViewThemeUtils.kt b/app/src/main/java/com/ionos/utils/IonosDialogViewThemeUtils.kt new file mode 100644 index 000000000000..371330456bf8 --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosDialogViewThemeUtils.kt @@ -0,0 +1,67 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.utils + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Build +import android.widget.ImageView +import android.widget.TextView +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.shape.MaterialShapeDrawable +import com.ionos.annotation.IonosCustomization +import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils +import com.owncloud.android.R + +class IonosDialogViewThemeUtils( + private val delegate: DialogViewThemeUtils, +) { + + @IonosCustomization + fun colorDialogMenuText(button: MaterialButton) { + button.setTextColor(button.context.getColor(R.color.text_color)) + button.iconTint = ColorStateList.valueOf(button.context.getColor(R.color.default_icon_color)) + } + + @IonosCustomization + fun colorMaterialAlertDialogBackground(context: Context, dialogBuilder: MaterialAlertDialogBuilder) { + val materialShapeDrawable = MaterialShapeDrawable( + context, + null, + com.google.android.material.R.attr.materialAlertDialogTheme, + com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents, + ) + materialShapeDrawable.initializeElevationOverlay(context) + materialShapeDrawable.fillColor = ColorStateList.valueOf(context.getColor(R.color.bg_default)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val radius = context.resources.getDimension(R.dimen.dialogBorderRadius) + materialShapeDrawable.setCornerSize(radius) + } + + dialogBuilder.background = materialShapeDrawable + } + + fun colorDialogHeadline(textView: TextView) { + delegate.colorDialogHeadline(textView) + } + + fun colorDialogIcon(icon: ImageView) { + delegate.colorDialogIcon(icon) + } + + fun colorDialogSupportingText(textView: TextView) { + delegate.colorDialogSupportingText(textView) + } + + fun colorMaterialAlertDialogIcon(context: Context, drawableId: Int): Drawable { + return delegate.colorMaterialAlertDialogIcon(context, drawableId) + } +} diff --git a/app/src/main/java/com/ionos/utils/IonosFilesSpecificViewThemeUtils.kt b/app/src/main/java/com/ionos/utils/IonosFilesSpecificViewThemeUtils.kt new file mode 100644 index 000000000000..18ab02258f1f --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosFilesSpecificViewThemeUtils.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.ionos.utils + +import android.content.Context +import android.preference.PreferenceCategory +import android.widget.ImageView +import androidx.annotation.StringRes +import androidx.appcompat.app.ActionBar +import androidx.appcompat.widget.SearchView +import com.google.android.material.card.MaterialCardView +import com.google.android.material.navigation.NavigationView +import com.google.android.material.textfield.MaterialAutoCompleteTextView +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase +import com.owncloud.android.R +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.utils.theme.FilesSpecificViewThemeUtils +import me.zhanghai.android.fastscroll.FastScrollerBuilder +import javax.inject.Inject + +@Suppress("TooManyFunctions") +class IonosFilesSpecificViewThemeUtils @Inject constructor( + schemes: MaterialSchemes, + private val delegate: FilesSpecificViewThemeUtils, +) : ViewThemeUtilsBase(schemes) { + + fun themePreferenceCategory(category: PreferenceCategory) { + // Do nothing + } + + @JvmOverloads + fun themeActionBar(context: Context, actionBar: ActionBar, title: String, isMenu: Boolean = false) { + val icon = getHomeAsUpIcon(isMenu) + actionBar.setHomeAsUpIndicator(icon) + actionBar.title = title + } + + @JvmOverloads + fun themeActionBar(context: Context, actionBar: ActionBar, isMenu: Boolean = false) { + val icon = getHomeAsUpIcon(isMenu) + actionBar.setHomeAsUpIndicator(icon) + } + + @JvmOverloads + fun themeActionBar(context: Context, actionBar: ActionBar, @StringRes titleRes: Int, isMenu: Boolean = false) { + val title = context.getString(titleRes) + themeActionBar(context, actionBar, title, isMenu) + } + + private fun getHomeAsUpIcon(isMenu: Boolean): Int { + val icon = if (isMenu) { + R.drawable.ic_menu + } else { + R.drawable.ic_arrow_back + } + return icon + } + + fun createAvatar(type: ShareType?, avatar: ImageView, context: Context) { + delegate.createAvatar(type, avatar, context) + } + + fun themeFastScrollerBuilder(context: Context, builder: FastScrollerBuilder): FastScrollerBuilder { + return delegate.themeFastScrollerBuilder(context, builder) + } + + fun themeTemplateCardView(cardView: MaterialCardView) { + delegate.themeTemplateCardView(cardView) + } + + fun themeStatusCardView(cardView: MaterialCardView) { + delegate.themeStatusCardView(cardView) + } + + fun themeAvatarButton(shareImageView: ImageView) { + delegate.themeAvatarButton(shareImageView) + } + + fun primaryColorToHexString(context: Context): String { + return delegate.primaryColorToHexString(context) + } + + fun setWhiteBackButton(context: Context, supportActionBar: ActionBar) { + delegate.setWhiteBackButton(context, supportActionBar) + } + + fun themeContentSearchView(searchView: SearchView) { + delegate.themeContentSearchView(searchView) + } + + fun themeAutoCompleteTextView(autoCompleteTextView: MaterialAutoCompleteTextView) { + delegate.themeAutoCompleteTextView(autoCompleteTextView) + } + + fun colorNavigationView(navigationView: NavigationView) { + } +} diff --git a/app/src/main/java/com/ionos/utils/IonosMaterialViewThemeUtils.kt b/app/src/main/java/com/ionos/utils/IonosMaterialViewThemeUtils.kt new file mode 100644 index 000000000000..b8cb18e297b0 --- /dev/null +++ b/app/src/main/java/com/ionos/utils/IonosMaterialViewThemeUtils.kt @@ -0,0 +1,159 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.ionos.utils + +import android.util.TypedValue +import androidx.core.content.ContextCompat +import androidx.core.view.setPadding +import androidx.core.view.updatePadding +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import com.google.android.material.chip.Chip +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.materialswitch.MaterialSwitch +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.tabs.TabLayout +import com.google.android.material.textfield.TextInputLayout +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils +import com.nextcloud.android.common.ui.util.buildColorStateList +import com.owncloud.android.R + +class IonosMaterialViewThemeUtils( + private val delegate: MaterialViewThemeUtils, +) { + + fun colorMaterialButtonPrimaryTonal(button: MaterialButton) { + colorMaterialButtonPrimaryFilled(button) + } + + fun colorMaterialButtonPrimaryBorderless(button: MaterialButton) { + colorMaterialButtonPrimaryOutlined(button) + } + + fun colorMaterialButtonPrimaryFilled(button: MaterialButton) { + button.backgroundTintList = ContextCompat.getColorStateList(button.context, R.color.filled_button_bg_color) + button.iconTint = ContextCompat.getColorStateList(button.context, R.color.filled_button_text_color) + button.setTextColor(ContextCompat.getColorStateList(button.context, R.color.filled_button_text_color)) + val textPadding = button.resources.getDimension(R.dimen.button_text_padding).toInt() + button.updatePadding(left = textPadding, right = textPadding) + } + + fun colorMaterialButtonPrimaryOutlined(button: MaterialButton) { + button.backgroundTintList = ContextCompat.getColorStateList(button.context, R.color.outlined_button_bg_color) + button.iconTint = ContextCompat.getColorStateList(button.context, R.color.outlined_button_text_color) + button.strokeColor = ContextCompat.getColorStateList(button.context, R.color.outlined_button_stroke_color) + button.strokeWidth = button.resources.getDimension(R.dimen.outlined_button_stroke_width).toInt() + button.setTextColor(ContextCompat.getColorStateList(button.context, R.color.outlined_button_text_color)) + val textPadding = button.resources.getDimension(R.dimen.button_text_padding).toInt() + button.updatePadding(left = textPadding, right = textPadding) + } + + fun colorMaterialButtonText(button: MaterialButton) { + button.setTextColor(ContextCompat.getColorStateList(button.context, R.color.outlined_button_text_color)) + } + + fun themeSnackbar(snackbar: Snackbar) {} + + fun colorTextInputLayout(textInputLayout: TextInputLayout) { + val context = textInputLayout.context + + val boxStrokeWidthRes = R.dimen.text_input_box_stroke_width + val cornerRadiusRes = R.dimen.text_input_box_corner_radius + val boxStrokeColorStateList = buildColorStateList( + -android.R.attr.state_focused to context.getColor(R.color.text_input_border_stroke_color), + android.R.attr.state_focused to context.getColor(R.color.text_input_focused_border_stroke_color), + ) + + textInputLayout.setBoxStrokeWidthResource(boxStrokeWidthRes) + textInputLayout.setBoxStrokeWidthFocusedResource(boxStrokeWidthRes) + textInputLayout.setBoxCornerRadiiResources(cornerRadiusRes, cornerRadiusRes, cornerRadiusRes, cornerRadiusRes) + textInputLayout.setBoxStrokeColorStateList(boxStrokeColorStateList) + + val errorColorStateList = buildColorStateList( + -android.R.attr.state_focused to context.getColor(R.color.text_input_error_color), + android.R.attr.state_focused to context.getColor(R.color.text_input_error_color), + ) + + textInputLayout.setErrorIconTintList(errorColorStateList) + textInputLayout.setErrorTextColor(errorColorStateList) + textInputLayout.boxStrokeErrorColor = errorColorStateList + + val hintTextColorStateList = buildColorStateList( + -android.R.attr.state_focused to context.getColor(R.color.text_input_hint_text_color), + android.R.attr.state_focused to context.getColor(R.color.text_input_focused_hint_text_color), + ) + + textInputLayout.defaultHintTextColor = hintTextColorStateList + + val padding = context.resources.getDimension(R.dimen.text_input_padding).toInt() + val textSize = context.resources.getDimension(R.dimen.text_input_text_size) + + textInputLayout.editText?.setPadding(padding) + textInputLayout.editText?.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + textInputLayout.editText?.highlightColor = context.getColor(R.color.text_input_highlight_color) + } + + fun themeFAB(fab: FloatingActionButton) { + delegate.themeFAB(fab) + } + + fun colorMaterialButtonFilledOnPrimary(btn: MaterialButton) { + delegate.colorMaterialButtonFilledOnPrimary(btn) + } + + fun colorMaterialButtonOutlinedOnPrimary(btn: MaterialButton) { + delegate.colorMaterialButtonOutlinedOnPrimary(btn) + } + + fun colorMaterialTextButton(btn: MaterialButton) { + delegate.colorMaterialTextButton(btn) + } + + fun colorProgressBar(progressBar: LinearProgressIndicator, primary: ColorRole = ColorRole.PRIMARY) { + delegate.colorProgressBar(progressBar, primary) + } + + fun colorQuotaProgressBar(progressBar: LinearProgressIndicator) { + colorProgressBar(progressBar, ColorRole.PRIMARY) + progressBar.trackColor = ContextCompat.getColor(progressBar.context, R.color.ionos_drawer_quota_background) + } + + fun themeChipSuggestion(chip: Chip) { + delegate.themeChipSuggestion(chip) + } + + fun colorTextInputLayout(til: TextInputLayout, colorRole: ColorRole) { + delegate.colorTextInputLayout(til, colorRole) + } + + fun themeTabLayout(tabLayout: TabLayout) { + delegate.themeTabLayout(tabLayout) + } + + fun colorMaterialSwitch(materialSwitch: MaterialSwitch) { + delegate.colorMaterialSwitch(materialSwitch) + } + + fun themeCardView(cardView: MaterialCardView) { + delegate.themeCardView(cardView) + } + + fun themeSearchCardView(cardView: MaterialCardView) { + delegate.themeSearchCardView(cardView) + } + + fun colorMaterialButtonContent(btn: MaterialButton, colorRole: ColorRole) { + delegate.colorMaterialButtonContent(btn, colorRole) + } + + fun themeToolbar(toolbar: MaterialToolbar) { + } +} diff --git a/app/src/main/java/com/ionos/utils/context/ContextUtils.kt b/app/src/main/java/com/ionos/utils/context/ContextUtils.kt new file mode 100644 index 000000000000..4b68b515e863 --- /dev/null +++ b/app/src/main/java/com/ionos/utils/context/ContextUtils.kt @@ -0,0 +1,18 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +@file:JvmName("ContextUtils") + +package com.ionos.utils.context + +import android.content.Context +import android.content.res.Configuration + +fun Context.isDarkMode(): Boolean { + val uiMode = resources.configuration.uiMode + return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES +} diff --git a/app/src/main/java/com/ionos/utils/text/CharSequenceUtils.kt b/app/src/main/java/com/ionos/utils/text/CharSequenceUtils.kt new file mode 100644 index 000000000000..c83acfa7965a --- /dev/null +++ b/app/src/main/java/com/ionos/utils/text/CharSequenceUtils.kt @@ -0,0 +1,46 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +@file:JvmName("CharSequenceUtils") + +package com.ionos.utils.text + +import android.text.Annotation +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.view.View + +fun CharSequence.convertAnnotatedTextToLinks( + linkColor: Int, + linkUnderline: Boolean, + linkHandler: (type: String) -> Unit, +): SpannableString { + val spannableString = SpannableString(this) + val annotations = spannableString.getSpans(0, spannableString.length, Annotation::class.java) + + annotations.forEach { annotation -> + val start = spannableString.getSpanStart(annotation) + val end = spannableString.getSpanEnd(annotation) + + val clickableSpan = object : ClickableSpan() { + override fun updateDrawState(paint: TextPaint) { + paint.color = linkColor + paint.isUnderlineText = linkUnderline + } + + override fun onClick(widget: View) { + linkHandler.invoke(annotation.value) + } + } + + spannableString.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + return spannableString +} diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java index 8b941d9f70e6..b64766574173 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java @@ -19,6 +19,8 @@ import android.preference.PreferenceManager; import android.text.TextUtils; +import com.ionos.annotation.IonosCustomization; +import com.ionos.authorization_method.AuthorizationMethodActivity; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.extensions.AccountExtensionsKt; @@ -458,12 +460,13 @@ private String getAccountType() { } @Override + @IonosCustomization public void startAccountCreation(final Activity activity) { // skipping AuthenticatorActivity redirection when user is on Launcher or FirstRun Activity if (activity instanceof LauncherActivity || activity instanceof FirstRunActivity) return; - Intent intent = new Intent(context, AuthenticatorActivity.class); + Intent intent = AuthorizationMethodActivity.createInstance(context); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index f8d5f3855c30..447119f1c0a4 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -9,6 +9,7 @@ package com.nextcloud.client.database.dao import androidx.room.Dao import androidx.room.Query +import com.ionos.annotation.IonosCustomization import androidx.room.Update import com.nextcloud.client.database.entity.FileEntity import com.owncloud.android.db.ProviderMeta.ProviderTableMeta @@ -68,6 +69,14 @@ interface FileDao { @Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") fun getAllFiles(fileOwner: String): List + @IonosCustomization + @Query( + "SELECT * FROM filelist WHERE favorite = 1" + + " AND file_owner = :fileOwner" + + " ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}" + ) + fun getFavoriteFiles(fileOwner: String): List + @Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC") fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List @@ -141,17 +150,6 @@ interface FileDao { ) suspend fun getSharedFiles(accountName: String): List - @Query( - """ - SELECT * - FROM filelist - WHERE file_owner = :fileOwner - AND favorite = 1 - ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} - """ - ) - suspend fun getFavoriteFiles(fileOwner: String): List - @Query("SELECT remote_id FROM filelist WHERE file_owner = :accountName AND remote_id IS NOT NULL") fun getAllRemoteIds(accountName: String): List diff --git a/app/src/main/java/com/nextcloud/client/di/AppComponent.java b/app/src/main/java/com/nextcloud/client/di/AppComponent.java index 8e1f599e5723..f9bcb421cb29 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppComponent.java +++ b/app/src/main/java/com/nextcloud/client/di/AppComponent.java @@ -9,6 +9,8 @@ import android.app.Application; +import com.ionos.annotation.IonosCustomization; +import com.ionos.di.StratoModule; import com.nextcloud.appReview.InAppReviewModule; import com.nextcloud.client.appinfo.AppInfoModule; import com.nextcloud.client.database.DatabaseModule; @@ -37,6 +39,7 @@ import dagger.Component; import dagger.android.support.AndroidSupportInjectionModule; +@IonosCustomization @Component(modules = { AndroidSupportInjectionModule.class, AppModule.class, @@ -53,6 +56,7 @@ DatabaseModule.class, DispatcherModule.class, VariantModule.class, + StratoModule.class, }) @Singleton public interface AppComponent { diff --git a/app/src/main/java/com/nextcloud/client/di/VariantModule.kt b/app/src/main/java/com/nextcloud/client/di/VariantModule.kt index 78899be6aec7..e6dcfdb1da4e 100644 --- a/app/src/main/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/VariantModule.kt @@ -9,6 +9,10 @@ package com.nextcloud.client.di import androidx.activity.result.contract.ActivityResultContract +import com.ionos.annotation.IonosCustomization +import com.ionos.scanbot.availability.Availability +import com.ionos.scanbot.di.qualifiers.Scanbot +import com.ionos.scanbot.di.qualifiers.ScanbotLicense import com.nextcloud.client.documentscan.AppScanOptionalFeature import dagger.Module import dagger.Provides @@ -16,30 +20,18 @@ import dagger.Reusable @Module internal class VariantModule { - /** - * Using reflection to determine whether the ScanPageContract class from the appscan project is available. - * If yes, an instance of it is returned. If not, a stub is returned indicating the feature is not available. - * - * To make it available for your specific variant, make sure it is included in your build.gradle, - * e.g.: `"qaImplementation"(project(":appscan"))` - */ + @IonosCustomization("Scanbot scan availability") @Provides @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature = try { - // Try to load the ScanPageContract class only if the appscan project is present - val clazz = Class.forName("com.nextcloud.appscan.ScanPageContract") - - @Suppress("UNCHECKED_CAST") - val contractInstance = - clazz.getDeclaredConstructor().newInstance() as ActivityResultContract - object : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract = contractInstance + fun scanOptionalFeature( + @Scanbot featureAvailability: Availability, + @ScanbotLicense licenseAvailability: Availability + ): AppScanOptionalFeature { + return object : AppScanOptionalFeature() { + override fun getScanContract(): ActivityResultContract { + throw UnsupportedOperationException("Document scan is not available") + } + override val isAvailable: Boolean = featureAvailability.available() && licenseAvailability.available() } - } catch (_: ClassNotFoundException) { - // appscan module is not present in this variant - AppScanOptionalFeature.Stub - } catch (_: Exception) { - // Any reflection/instantiation error -> be safe and use stub - AppScanOptionalFeature.Stub } } diff --git a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt index 48920e1ab858..f99e940fa46f 100644 --- a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt +++ b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt @@ -19,6 +19,7 @@ import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import com.canhub.cropper.CropImageView +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -32,7 +33,9 @@ import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.theme.ViewThemeUtils import java.io.File +import javax.inject.Inject class EditImageActivity : FileActivity(), @@ -45,6 +48,9 @@ class EditImageActivity : private lateinit var file: OCFile private lateinit var format: Bitmap.CompressFormat + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + companion object { const val EXTRA_FILE = "FILE" const val OPEN_IMAGE_EDITOR = "OPEN_IMAGE_EDITOR" @@ -61,6 +67,7 @@ class EditImageActivity : fun canBePreviewed(file: OCFile): Boolean = file.mimeType in supportedMimeTypes } + @IonosCustomization override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,7 +86,7 @@ class EditImageActivity : val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) - window.navigationBarColor = getColor(R.color.black) + viewThemeUtils.platform.themeStatusBar(this,getColor(R.color.edit_image_background)) setupCropper() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt index ebcb5f7dd4d3..408a13575bfc 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt @@ -17,6 +17,7 @@ import android.text.TextUtils import androidx.work.Worker import androidx.work.WorkerParameters import com.google.gson.Gson +import com.ionos.privacy.PrivacyPreferences import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock @@ -58,7 +59,8 @@ class AccountRemovalWork( private val clock: Clock, private val eventBus: EventBus, private val preferences: AppPreferences, - private val syncedFolderProvider: SyncedFolderProvider + private val syncedFolderProvider: SyncedFolderProvider, + private val privacyPreferences: PrivacyPreferences, ) : Worker(context, params) { companion object { @@ -112,6 +114,8 @@ class AccountRemovalWork( preferences.currentAccountName = "" } + privacyPreferences.removeDataProtectionProcessed(accountName) + // remove all files storageManager.removeLocalFiles(user, storageManager) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b9145d7e8769..6f20ffa04928 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -15,6 +15,9 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters +import com.ionos.privacy.PrivacyPreferences +import com.ionos.scanbot.license.ScanbotLicenseDownloadWorker +import com.ionos.scanbot.license.ScanbotLicenseJobFactory import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock import com.nextcloud.client.database.NextcloudDatabase @@ -67,6 +70,8 @@ class BackgroundJobFactory @Inject constructor( private val localBroadcastManager: Provider, private val generatePdfUseCase: GeneratePDFUseCase, private val syncedFolderProvider: SyncedFolderProvider, + private val scanbotLicenseJobFactory: ScanbotLicenseJobFactory, + private val privacyPreferences: PrivacyPreferences, private val database: NextcloudDatabase, private val uploadFileOperationFactory: UploadFileOperationFactory ) : WorkerFactory() { @@ -100,6 +105,7 @@ class BackgroundJobFactory @Inject constructor( FilesExportWork::class -> createFilesExportWork(context, workerParameters) FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) + ScanbotLicenseDownloadWorker::class -> scanbotLicenseJobFactory.create(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) @@ -233,7 +239,8 @@ class BackgroundJobFactory @Inject constructor( clock, eventBus, preferences, - syncedFolderProvider + syncedFolderProvider, + privacyPreferences, ) private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FileUploadWorker = diff --git a/app/src/main/java/com/nextcloud/client/media/Player.kt b/app/src/main/java/com/nextcloud/client/media/Player.kt index 0074690ea87d..5103019b64a4 100644 --- a/app/src/main/java/com/nextcloud/client/media/Player.kt +++ b/app/src/main/java/com/nextcloud/client/media/Player.kt @@ -253,9 +253,7 @@ internal class Player( override fun getBufferPercentage(): Int = 0 override fun seekTo(pos: Int) { - if (stateMachine.isInState(State.PLAYING)) { - mediaPlayer?.seekTo(pos) - } + mediaPlayer?.seekTo(pos) } override fun getCurrentPosition(): Int = mediaPlayer?.currentPosition ?: 0 diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt index 15e6398351c0..ccb8dcdbb17b 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt @@ -7,8 +7,11 @@ package com.nextcloud.client.media import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.Service import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT +import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP import android.media.AudioManager import android.os.Bundle import android.os.IBinder @@ -16,6 +19,7 @@ import android.widget.MediaController import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.ForegroundServiceHelper @@ -200,9 +204,14 @@ class PlayerService : Service() { stopServiceAndRemoveNotification(file) } + @IonosCustomization("clickable notification") private fun startForeground(currentFile: OCFile) { val ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name)) val content = getString(R.string.media_state_playing, currentFile.getFileName()) + val intent = Intent(this, PreviewMediaActivity::class.java) + .apply { + flags = FLAG_ACTIVITY_REORDER_TO_FRONT or FLAG_ACTIVITY_SINGLE_TOP + } notificationBuilder.run { setSmallIcon(R.drawable.ic_play_arrow) @@ -210,6 +219,9 @@ class PlayerService : Service() { setOngoing(true) setContentTitle(ticker) setContentText(content) + setContentIntent( + PendingIntent.getActivity(mContext, 0, intent, FLAG_IMMUTABLE) + ) setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA) } diff --git a/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt b/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt index 8acc663a58b1..ea6fb353b606 100644 --- a/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt +++ b/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt @@ -11,6 +11,7 @@ import android.accounts.Account import android.app.Activity import android.content.Intent import android.os.Bundle +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.utils.extensions.isAnonymous @@ -26,8 +27,12 @@ import java.util.Optional * It is an intermediary step facilitating comprehensive rework of * account handling logic. */ -class SessionMixin(private val activity: Activity, private val accountManager: UserAccountManager) : ActivityMixin { - var currentAccount: Account = getDefaultAccount() +class SessionMixin( + private val activity: Activity, + private val accountManager: UserAccountManager +) : ActivityMixin { + @IonosCustomization("Fix double call of startAccountCreation on activity create") + var currentAccount: Account = accountManager.currentAccount private set companion object { diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 709f368080d8..435f502d1dbe 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -17,11 +17,14 @@ import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.appReview.AppReviewShownModel; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; import com.nextcloud.client.jobs.LogEntry; +import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -518,7 +521,12 @@ public void setUploaderBehaviour(int uploaderBehaviour) { * @return grid columns grid columns */ @Override + @IonosCustomization public float getGridColumns() { + if (IonosBuildHelper.isIonosBuild()) { + return context.getResources().getInteger(R.integer.grid_mode_column_count); + } + float columns = preferences.getFloat(AUTO_PREF__GRID_COLUMNS, DEFAULT_GRID_COLUMN); if (columns < 0) { diff --git a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt index efd2c7bb4e0a..93f6a6924eaa 100644 --- a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt @@ -20,6 +20,7 @@ import android.widget.ImageView import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -84,6 +85,7 @@ class ChooseAccountDialogFragment : return builder.create() } + @IonosCustomization("Hide account id") @Suppress("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -105,6 +107,7 @@ class ChooseAccountDialogFragment : binding.currentAccount.userName.text = user.toOwnCloudAccount().displayName binding.currentAccount.ticker.visibility = View.GONE binding.currentAccount.account.text = user.accountName + binding.currentAccount.account.visibility = View.GONE // Defining user right indicator val icon = viewThemeUtils.platform.tintPrimaryDrawable(requireContext(), R.drawable.ic_check_circle) diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index f30dbefbe86a..cd4bc7d23ec8 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -11,9 +11,11 @@ package com.nextcloud.ui.fileactions import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.StringRes +import com.ionos.annotation.IonosCustomization import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile +@IonosCustomization("Custom icons") enum class FileAction( @param:IdRes val id: Int, @param:StringRes val title: Int, @@ -24,14 +26,14 @@ enum class FileAction( SELECT_NONE(R.id.action_deselect_all_action_menu, R.string.deselect_all, R.drawable.ic_select_none), // generic file actions - EDIT(R.id.action_edit, R.string.action_edit, R.drawable.ic_edit), + EDIT(R.id.action_edit, R.string.action_edit, R.drawable.ic_file_action_edit), SEE_DETAILS(R.id.action_see_details, R.string.actionbar_see_details, R.drawable.ic_information_outline), REMOVE_FILE(R.id.action_remove_file, R.string.common_remove, R.drawable.ic_delete), LEAVE_SHARE(R.id.action_remove_file, R.string.common_leave_this_share, R.drawable.ic_cancel), // File moving RENAME_FILE(R.id.action_rename_file, R.string.common_rename, R.drawable.ic_rename), - MOVE_OR_COPY(R.id.action_move_or_copy, R.string.actionbar_move_or_copy, R.drawable.ic_external), + MOVE_OR_COPY(R.id.action_move_or_copy, R.string.actionbar_move_or_copy, R.drawable.ic_file_action_move_or_copy), // favorites FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star_outline), @@ -39,25 +41,26 @@ enum class FileAction( // Uploads and downloads DOWNLOAD_FILE(R.id.action_download_file, R.string.filedetails_download, R.drawable.ic_cloud_download), + SYNC_FILE(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_file_action_sync_file), SYNC_FOLDER(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_sync), SYNC_ALL_FOLDERS(R.id.action_sync_all_files, R.string.filedetails_sync_all_files, R.drawable.ic_sync_all), - CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_sync_off), + CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_file_action_cancel_sync), // File sharing EXPORT_FILE(R.id.action_export_file, R.string.filedetails_export, R.drawable.ic_export), SEND_SHARE_FILE(R.id.action_send_share_file, R.string.action_send_share, R.drawable.ic_share), SEND_FILE(R.id.action_send_file, R.string.common_send, R.drawable.ic_share), - OPEN_FILE_WITH(R.id.action_open_file_with, R.string.actionbar_open_with, R.drawable.ic_external), - STREAM_MEDIA(R.id.action_stream_media, R.string.stream, R.drawable.ic_play_arrow), + OPEN_FILE_WITH(R.id.action_open_file_with, R.string.actionbar_open_with, R.drawable.ic_file_action_open_file_with), + STREAM_MEDIA(R.id.action_stream_media, R.string.stream, R.drawable.ic_file_action_stream_media), SET_AS_WALLPAPER(R.id.action_set_as_wallpaper, R.string.set_picture_as, R.drawable.ic_wallpaper), // Encryption - SET_ENCRYPTED(R.id.action_encrypted, R.string.encrypted, R.drawable.ic_encrypt), - UNSET_ENCRYPTED(R.id.action_unset_encrypted, R.string.unset_encrypted, R.drawable.ic_decrypt), + SET_ENCRYPTED(R.id.action_encrypted, R.string.encrypted, R.drawable.ic_file_action_set_encrypted), + UNSET_ENCRYPTED(R.id.action_unset_encrypted, R.string.unset_encrypted, R.drawable.ic_file_action_unset_encrypted), // locks - UNLOCK_FILE(R.id.action_unlock_file, R.string.unlock_file, R.drawable.ic_lock_open_white), - LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock), + UNLOCK_FILE(R.id.action_unlock_file, R.string.unlock_file, R.drawable.ic_file_action_unlock_file), + LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_file_action_lock_file), // Shortcuts PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen), diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt index 733b87b1693a..95c335595f4d 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt @@ -17,7 +17,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.IdRes -import androidx.appcompat.content.res.AppCompatResources import androidx.core.os.BundleCompat import androidx.core.os.bundleOf import androidx.core.view.isNotEmpty @@ -29,6 +28,7 @@ import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.lib.resources.clientintegration.Endpoint import com.nextcloud.client.account.CurrentAccountProvider @@ -165,11 +165,9 @@ class FileActionsBottomSheet : } } + @IonosCustomization("Set thumbnail drawable without tint") private fun setMultipleFilesThumbnail() { - context?.let { - val drawable = viewThemeUtils.platform.tintDrawable(it, R.drawable.file_multiple, ColorRole.PRIMARY) - binding.thumbnailLayout.thumbnail.setImageDrawable(drawable) - } + binding.thumbnailLayout.thumbnail.setImageResource(R.drawable.file_multiple) } override fun onDestroyView() { @@ -320,6 +318,7 @@ class FileActionsBottomSheet : binding.title.text = resources.getQuantityString(R.plurals.file_list__footer__file, fileCount, fileCount) } + @IonosCustomization("Set icon drawable without tint") private fun inflateActionView(action: FileAction): View { val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false) .apply { @@ -328,12 +327,7 @@ class FileActionsBottomSheet : } text.setText(action.title) if (action.icon != null) { - val drawable = - viewThemeUtils.platform.tintDrawable( - requireContext(), - AppCompatResources.getDrawable(requireContext(), action.icon)!! - ) - icon.setImageDrawable(drawable) + icon.setImageResource(action.icon) } } return itemBinding.root diff --git a/app/src/main/java/com/nextcloud/utils/BuildHelper.kt b/app/src/main/java/com/nextcloud/utils/BuildHelper.kt index 2d17528e83c3..fa905776ea20 100644 --- a/app/src/main/java/com/nextcloud/utils/BuildHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/BuildHelper.kt @@ -9,7 +9,12 @@ package com.nextcloud.utils import com.owncloud.android.BuildConfig object BuildHelper { - fun isFlavourGPlay(): Boolean = BuildConfig.FLAVOR == "gplay" + const val GPLAY: String = "gplay" + const val HIDRIVE_NEXT: String = "hidriveNext" + + private val GPLAY_FLAVOURS = setOf(GPLAY, HIDRIVE_NEXT) + + fun isFlavourGPlay(): Boolean = BuildConfig.FLAVOR in GPLAY_FLAVOURS fun isHuaweiFlavor(): Boolean = BuildConfig.FLAVOR == "huawei" } diff --git a/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt b/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt index b39b3f812299..5f5be28b8327 100644 --- a/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt +++ b/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt @@ -16,14 +16,15 @@ import android.text.TextUtils import android.view.View import androidx.annotation.VisibleForTesting import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.ionos.annotation.IonosCustomization +import com.ionos.authorization_method.AuthorizationMethodActivity +import com.ionos.privacy.DataProtectionActivity +import com.ionos.privacy.PrivacyPreferences import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R -import com.owncloud.android.authentication.AuthenticatorActivity import com.owncloud.android.databinding.ActivitySplashBinding import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.activity.SettingsActivity import javax.inject.Inject class LauncherActivity : BaseActivity() { @@ -33,6 +34,9 @@ class LauncherActivity : BaseActivity() { @Inject lateinit var appPreferences: AppPreferences + @Inject + lateinit var privacyPreferences: PrivacyPreferences + override fun onCreate(savedInstanceState: Bundle?) { // Mandatory to call this before super method to show system launch screen for api level 31+ installSplashScreen() @@ -46,6 +50,9 @@ class LauncherActivity : BaseActivity() { scheduleSplashScreen() } + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + @VisibleForTesting fun setSplashTitles(boldText: String, normalText: String) { binding.splashScreenBold.visibility = View.VISIBLE @@ -64,16 +71,18 @@ class LauncherActivity : BaseActivity() { } } + @IonosCustomization private fun scheduleSplashScreen() { Handler(Looper.getMainLooper()).postDelayed({ if (user.isPresent) { - if (MDMConfig.enforceProtection(this) && appPreferences.lockPreference == SettingsActivity.LOCK_NONE) { - startActivity(Intent(this, SettingsActivity::class.java)) + val intent = Intent(this, FileDisplayActivity::class.java) + if (privacyPreferences.isDataProtectionProcessed(userAccountManager.currentOwnCloudAccount?.name)) { + startActivity(intent) } else { - startActivity(Intent(this, FileDisplayActivity::class.java)) + startActivity(DataProtectionActivity.createIntent(this, intent)) } } else { - startActivity(Intent(this, AuthenticatorActivity::class.java)) + startActivity(AuthorizationMethodActivity.createInstance(this)) } finish() }, SPLASH_DURATION) diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index 8caff5691b66..7956bd092f9a 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -52,6 +52,10 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; +import com.ionos.annotation.IonosCustomization; +import com.ionos.branding.BrandConfigProvider; +import com.ionos.privacy.DataProtectionActivity; +import com.ionos.privacy.PrivacyPreferences; import com.nextcloud.android.common.ui.color.ColorUtil; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; @@ -226,12 +230,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity @Inject UserAccountManager accountManager; @Inject AppPreferences preferences; + @Inject PrivacyPreferences privacyPreferences; @Inject OnboardingService onboarding; @Inject DeviceInfo deviceInfo; @Inject PassCodeManager passCodeManager; @Inject ViewThemeUtils.Factory viewThemeUtilsFactory; @Inject ColorUtil colorUtil; @Inject ClientFactory clientFactory; + @Inject BrandConfigProvider brandConfigProvider; private AuthObject authObject = null; private String fallbackToken; @@ -243,6 +249,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); protected LoginDialog loginDialog; + private String getResolvedLoginV2Url() { + return IonosLoginDomainResolver.resolveLoginV2Url(this, brandConfigProvider); + } + @VisibleForTesting public AccountSetupBinding getAccountSetupBinding() { return accountSetupBinding; @@ -254,17 +264,16 @@ public AccountSetupBinding getAccountSetupBinding() { * IMPORTANT ENTRY POINT 1: activity is shown to the user */ @Override + @IonosCustomization("Removed the coloring of the status bar to match the app style") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); loginDialog = new LoginDialog(this); viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground(); - viewThemeUtils.platform.colorStatusBar(this, getResources().getColor(R.color.primary)); + + WebViewUtil webViewUtil = new WebViewUtil(); Uri data = getIntent().getData(); boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme)); - if (savedInstanceState == null && !directLogin) { - onboarding.launchFirstRunIfNeeded(this); - } onlyAdd = getIntent().getBooleanExtra(KEY_ONLY_ADD, false) || checkIfViaSSO(getIntent()); @@ -300,6 +309,10 @@ protected void onCreate(Bundle savedInstanceState) { mIsFirstAuthAttempt = savedInstanceState.getBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG); } + if (directLogin) { + return; + } + boolean webViewLoginMethod = false; String webloginUrl = null; @@ -314,10 +327,10 @@ protected void onCreate(Bundle savedInstanceState) { webViewLoginMethod = true; } else if (getIntent().getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { webViewLoginMethod = true; - webloginUrl = getString(R.string.provider_registration_server); + webloginUrl = getResolvedLoginV2Url(); } else if (!TextUtils.isEmpty(getResources().getString(R.string.webview_login_url))) { webViewLoginMethod = true; - webloginUrl = getResources().getString(R.string.webview_login_url); + webloginUrl = getResolvedLoginV2Url(); showWebViewLoginUrl = getResources().getBoolean(R.bool.show_server_url_input); } @@ -341,13 +354,11 @@ protected void onCreate(Bundle savedInstanceState) { } else { showEnforcedServers(); } - - initServerPreFragment(savedInstanceState); - } - - ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleEventObserver); + } + initServerPreFragment(savedInstanceState); + //ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleEventObserver); } - + private void showEnforcedServers() { showAuthStatus(); accountSetupBinding.hostUrlFrame.setVisibility(View.GONE); @@ -392,6 +403,13 @@ public void onNothingSelected(AdapterView parent) { }); } + private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { + if (event == Lifecycle.Event.ON_START && authObject != null && !TextUtils.isEmpty(authObject.getPoll().getToken())) { + Log_OC.d(TAG, "Start poolLogin"); + poolLogin(); + } + }); + private void deleteCookies() { try { CookieManager.getInstance().removeAllCookies(null); @@ -424,6 +442,7 @@ private void poolLogin() { * It's typically the entry point for the login process. * Example: "..." */ + @IonosCustomization("Use Webview login instead of default browser") private void anonymouslyPostLoginRequest(String url) { if (TextUtils.isEmpty(url)) { DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_empty_base_url); @@ -440,8 +459,7 @@ private void anonymouslyPostLoginRequest(String url) { String loginUrl = extractLoginUrl(response); runOnUiThread(() -> { - initLoginInfoView(); - launchDefaultWebBrowser(loginUrl); + initWebViewLogin(loginUrl); }); }); } @@ -471,7 +489,7 @@ private String extractLoginUrl(String response) { Log_OC.e(TAG, "Both AuthObject and fallback parsing failed, returning default login URL"); DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_login_error); - return getResources().getString(R.string.webview_login_url); + return getResolvedLoginV2Url(); } private String getLoginFromJsonObject(String response) { @@ -519,6 +537,46 @@ private void launchDefaultWebBrowser(String url) { DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_no_web_browser_found); } + @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") + @SuppressLint("SetJavaScriptEnabled") + @IonosCustomization("Set LoginWebViewContainer visibility") + private void initWebViewLogin(String baseURL) { + viewThemeUtils.platform.colorCircularProgressBar(accountSetupWebviewBinding.loginWebviewProgressBar, ColorRole.ON_PRIMARY_CONTAINER); + accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); + getLoginWebViewContainer().ifPresent(it -> it.setVisibility(View.GONE)); + + if (accountSetupBinding != null) { + accountSetupBinding.hostUrlInput.setText(""); + } + + new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); + + accountSetupWebviewBinding.loginWebview.getSettings().setAllowFileAccess(false); + accountSetupWebviewBinding.loginWebview.getSettings().setJavaScriptEnabled(true); + accountSetupWebviewBinding.loginWebview.getSettings().setDomStorageEnabled(true); + + accountSetupWebviewBinding.loginWebview.getSettings().setUserAgentString(MainApp.getUserAgent()); + accountSetupWebviewBinding.loginWebview.getSettings().setSaveFormData(false); + accountSetupWebviewBinding.loginWebview.getSettings().setSavePassword(false); + + Map headers = new HashMap<>(); + headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); + + String url; + if (baseURL != null && !baseURL.isEmpty()) { + url = baseURL; + } else { + url = getResolvedLoginV2Url(); + } + + new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); + + accountSetupWebviewBinding.loginWebview.loadUrl(url, headers); + accountSetupWebviewBinding.loginFlowV2.loginFlowInfoV2.setVisibility(View.GONE); + + setClient(); + } + private Pair extractPollUrlAndToken() { if (authObject != null) { final var poll = authObject.getPoll(); @@ -539,7 +597,7 @@ private Pair extractPollUrlAndToken() { return new Pair<>(baseUrl + "/poll", fallbackToken); } - + private void performLoginFlowV2() { final var pollUrlAndToken = extractPollUrlAndToken(); @@ -587,13 +645,6 @@ private void completeLoginFlow(String response, int status) { loginFlowExecutorService.shutdown(); ProcessLifecycleOwner.get().getLifecycle().removeObserver(lifecycleEventObserver); } - - private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { - if (event == Lifecycle.Event.ON_START && authObject != null && !TextUtils.isEmpty(authObject.getPoll().getToken())) { - Log_OC.d(TAG, "Start poolLogin"); - poolLogin(); - } - }); // endregion @Override @@ -623,17 +674,34 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request } @Override + @IonosCustomization("Set LoginWebViewContainer visibility") public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); accountSetupWebviewBinding.loginWebviewProgressBar.setVisibility(View.GONE); accountSetupWebviewBinding.loginWebview.setVisibility(View.VISIBLE); + getLoginWebViewContainer().ifPresent(it -> it.setVisibility(View.VISIBLE)); + + viewThemeUtils.platform.resetStatusBar(AuthenticatorActivity.this); + if (url.contains("/lf2-consent") || url.contains("/lf2-success")) { + finishLoginProcedure(); + } + } + + private void finishLoginProcedure() { + loginFlowExecutorService.execute(() -> { + if (!isLoginProcessCompleted) { + performLoginFlowV2(); + } + }); } @Override + @IonosCustomization("Set LoginWebViewContainer visibility") public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { accountSetupWebviewBinding.loginWebviewProgressBar.setVisibility(View.GONE); accountSetupWebviewBinding.loginWebview.setVisibility(View.VISIBLE); + getLoginWebViewContainer().ifPresent(it -> it.setVisibility(View.VISIBLE)); InputStream resources = getResources().openRawResource(R.raw.custom_error); String customError = DisplayUtils.getData(resources); @@ -746,7 +814,7 @@ private void initServerPreFragment(Bundle savedInstanceState) { mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); mServerInfo.mVersion = accountManager.getServerVersion(mAccount); } else { - mServerInfo.mBaseUrl = getString(R.string.webview_login_url).trim(); + mServerInfo.mBaseUrl = getResolvedLoginV2Url().trim(); mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); } } else { @@ -839,6 +907,7 @@ public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { * AndroidManifest.xml file. */ @Override + @IonosCustomization protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log_OC.d(TAG, "onNewIntent()"); @@ -864,10 +933,11 @@ protected void onNewIntent(Intent intent) { } } - if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { + if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, true)) { accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initSimpleSignupLogin(); + anonymouslyPostLoginRequest(getResolvedLoginV2Url()); + //initSimpleSignupLogin(); } } @@ -891,7 +961,7 @@ private void initSimpleSignupLogin() { new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); - accountSetupWebviewBinding.loginWebview.loadUrl(getString(R.string.provider_registration_server), headers); + accountSetupWebviewBinding.loginWebview.loadUrl(getResolvedLoginV2Url(), headers); accountSetupWebviewBinding.loginFlowV2.loginFlowInfoV2.setVisibility(View.GONE); setClient(); @@ -1013,8 +1083,15 @@ private void checkOcServer() { * Tests the credentials entered by the user performing a check of existence on the root folder of the ownCloud * server. */ + @IonosCustomization private void checkBasicAuthorization(@Nullable String webViewUsername, @Nullable String webViewPassword) { - OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(webViewUsername, webViewPassword); + if (accountSetupWebviewBinding != null) { + accountSetupWebviewBinding.loginFlowV2.tvAuthorizationDescription.setText(R.string.ionos_auth_trying_to_login); + } + + // validate credentials accessing the root folder + OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(webViewUsername, + webViewPassword); accessRootFolder(credentials); } @@ -1098,12 +1175,12 @@ private void onGetServerInfoFinish(RemoteOperationResult result) { setContentView(accountSetupWebviewBinding.getRoot()); if (!isLoginProcessCompleted) { - if (!isRedirectedToTheDefaultBrowser) { - anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); - isRedirectedToTheDefaultBrowser = true; - } else { - initLoginInfoView(); - } +// if (!isRedirectedToTheDefaultBrowser) { +// anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); +// isRedirectedToTheDefaultBrowser = true; +// } else { +// initLoginInfoView(); +// } } } } else { @@ -1118,25 +1195,11 @@ private void onGetServerInfoFinish(RemoteOperationResult result) { } // region LoginInfoView + @IonosCustomization private void initLoginInfoView() { - LinearLayout loginFlowLayout = accountSetupWebviewBinding.loginFlowV2.getRoot(); - MaterialButton cancelButton = accountSetupWebviewBinding.loginFlowV2.cancelButton; - loginFlowLayout.setVisibility(View.VISIBLE); - - // add bottom padding to prevent overlapping with system bars - ViewCompat.setOnApplyWindowInsetsListener(loginFlowLayout, (view, insets) -> { - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - view.setPadding( - view.getPaddingLeft(), - view.getPaddingTop(), - view.getPaddingRight(), - systemBars.bottom); - return insets; - }); - // the listener is attached after the initial insets dispatch, so request a fresh pass - ViewCompat.requestApplyInsets(loginFlowLayout); + MaterialButton retryButton = accountSetupWebviewBinding.loginFlowV2.bRetry; - cancelButton.setOnClickListener(v -> { + retryButton.setOnClickListener(v -> { loginFlowExecutorService.shutdown(); ProcessLifecycleOwner.get().getLifecycle().removeObserver(lifecycleEventObserver); recreate(); @@ -1327,6 +1390,7 @@ private void updateStatusIconFailUserName(int failedStatusText) { * @param result Result of the operation. */ @Override + @IonosCustomization("Set LoginWebViewContainer visibility") public void onAuthenticatorTaskCallback(RemoteOperationResult result) { mWaitingForOpId = Long.MAX_VALUE; dismissWaitingDialog(); @@ -1360,6 +1424,11 @@ public void onAuthenticatorTaskCallback(RemoteOperationResult result) accountManager.setCurrentOwnCloudAccount(mAccount.name); getUserCapabilitiesAndFinish(); } else { + // init webView again + if (accountSetupWebviewBinding != null) { + accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); + getLoginWebViewContainer().ifPresent(it -> it.setVisibility(View.GONE)); + } accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater()); setContentView(accountSetupBinding.getRoot()); initOverallUi(); @@ -1403,17 +1472,19 @@ public void onAuthenticatorTaskCallback(RemoteOperationResult result) } } + @IonosCustomization private void endSuccess() { if (!onlyAdd) { - if (MDMConfig.INSTANCE.enforceProtection(this) && Objects.equals(preferences.getLockPreference(), SettingsActivity.LOCK_NONE)) { - Intent i = new Intent(this, SettingsActivity.class); - startActivity(i); - } else { - Intent i = new Intent(this, FileDisplayActivity.class); - i.setAction(FileDisplayActivity.RESTART); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); + Intent i = new Intent(this, FileDisplayActivity.class); + i.setAction(FileDisplayActivity.RESTART); + String accountName = accountManager.getCurrentOwnCloudAccount() != null + ? accountManager.getCurrentOwnCloudAccount().getName() + : null; + if (!privacyPreferences.isDataProtectionProcessed(accountName)) { + i = DataProtectionActivity.createIntent(this, i); } + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(i); } finish(); @@ -1773,4 +1844,13 @@ public void onSavedCertificate() { public void onFailedSavingCertificate() { DisplayUtils.showSnackMessage(this, R.string.ssl_validator_not_saved); } + + @IonosCustomization("LoginWebViewContainer for consuming window insets") + private Optional getLoginWebViewContainer() { + int containerId = getResources().getIdentifier("login_webview_container", "id", getPackageName()); + if (containerId != 0) { + return Optional.ofNullable(findViewById(containerId)); + } + return Optional.empty(); + } } diff --git a/app/src/main/java/com/owncloud/android/authentication/IonosLoginDomainResolver.java b/app/src/main/java/com/owncloud/android/authentication/IonosLoginDomainResolver.java new file mode 100644 index 000000000000..ca48da0c9a49 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/authentication/IonosLoginDomainResolver.java @@ -0,0 +1,51 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2026 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ +package com.owncloud.android.authentication; + +import android.content.Context; + +import com.ionos.branding.BrandConfigProvider; +import com.ionos.branding.LoginUrls; + +import androidx.annotation.NonNull; +import androidx.core.os.ConfigurationCompat; + +import java.util.Locale; + +/** + * Resolves the web login URL for the current device locale from the brand configuration + * (see {@link com.ionos.branding.BrandConfig}). + */ +public final class IonosLoginDomainResolver { + + private IonosLoginDomainResolver() { + // Utility class. + } + + @NonNull + public static String resolveLoginV2Url(Context context, @NonNull BrandConfigProvider brandConfigProvider) { + return resolveLoginV2Url(getDeviceLocale(context), brandConfigProvider.getConfig().getLoginUrls()); + } + + @NonNull + public static String resolveLoginV2Url(@NonNull Locale locale, @NonNull LoginUrls loginUrls) { + String country = locale.getCountry().toUpperCase(Locale.ROOT); + String byCountry = loginUrls.getByCountry().get(country); + if (byCountry != null) { + return byCountry; + } + + String language = locale.getLanguage().toLowerCase(Locale.ROOT); + String byLanguage = loginUrls.getByLanguage().get(language); + return byLanguage != null ? byLanguage : loginUrls.getDefault(); + } + + private static Locale getDeviceLocale(Context context) { + Locale locale = ConfigurationCompat.getLocales(context.getResources().getConfiguration()).get(0); + return locale != null ? locale : Locale.getDefault(); + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 0b44558170cd..718f6432c3e7 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -31,6 +31,7 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.lib.resources.files.FileDownloadLimit; import com.nextcloud.client.account.User; import com.nextcloud.client.database.NextcloudDatabase; @@ -1992,6 +1993,36 @@ private ArrayList prepareRemoveSharesInFile( } + @IonosCustomization + public List getShares() { + String selection = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + " = ?"; + String[] selectionArgs = new String[]{user.getAccountName()}; + + Cursor cursor = null; + Uri uri = ProviderTableMeta.CONTENT_URI_SHARE; + if (getContentResolver() != null) { + cursor = getContentResolver().query(uri, null, selection, selectionArgs, null); + } else { + try { + cursor = getContentProviderClient().query(uri, null, selection, selectionArgs, null); + } catch (RemoteException e) { + Log_OC.e(TAG, "Could not get list of shares: " + e.getMessage(), e); + } + } + + ArrayList shares = new ArrayList<>(); + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + shares.add(createShareInstance(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + } + + return shares; + } + public List getSharesWithForAFile(String filePath, String accountName) { String selection = ProviderTableMeta.OCSHARES_PATH + AND + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + AND @@ -2836,6 +2867,18 @@ public List getAllFiles() { return folderContent; } + @IonosCustomization + public List getFavoriteFiles() { + List fileEntities = fileDao.getFavoriteFiles(user.getAccountName()); + List favoriteFiles = new ArrayList<>(fileEntities.size()); + + for (FileEntity fileEntity : fileEntities) { + favoriteFiles.add(createFileInstance(fileEntity)); + } + + return favoriteFiles; + } + private String getString(Cursor cursor, String columnName) { return cursor.getString(cursor.getColumnIndexOrThrow(columnName)); } diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index f27dbe409932..fe3d1145bc71 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -20,6 +20,7 @@ import android.os.Parcelable; import android.text.TextUtils; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.utils.BuildHelper; import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.R; @@ -700,9 +701,12 @@ private boolean hasPermission(String permission) { return permissions != null && permissions.contains(permission); } + @IonosCustomization("Icon for all sharing types") public Integer getFileOverlayIconId(boolean isAutoUploadFolder) { if (WebdavEntry.MountType.GROUP == mountType || mounted()) { return R.drawable.ic_folder_overlay_account_group; + } else if (sharedViaLink && !encrypted && (isSharedWithMe() || sharedWithSharee)) { + return R.drawable.ic_folder_all_share_types; } else if (sharedViaLink && !encrypted) { return R.drawable.ic_folder_overlay_link; } else if (isSharedWithMe() || sharedWithSharee) { diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index 66b215e733dc..407ad52a4459 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -35,6 +35,8 @@ import android.widget.FrameLayout; import android.widget.ImageView; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.client.account.User; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.BitmapExtensionsKt; @@ -531,6 +533,7 @@ protected Bitmap doInBackground(ThumbnailGenerationTaskObject... params) { return thumbnail; } + @IonosCustomization protected void onPostExecute(Bitmap bitmap) { if (bitmap != null && mImageViewReference != null) { final ImageView imageView = mImageViewReference.get(); @@ -546,7 +549,9 @@ protected void onPostExecute(Bitmap bitmap) { tagId = String.valueOf(((TrashbinFile) mFile).getRemoteId()); } if (String.valueOf(imageView.getTag()).equals(tagId)) { - if (gridViewEnabled) { + if (IonosBuildHelper.isIonosBuild()) { + imageView.setImageBitmap(bitmap); + } else if (gridViewEnabled) { BitmapUtils.setRoundedBitmapForGridMode(bitmap, imageView); } else { BitmapUtils.setRoundedBitmap(bitmap, imageView); @@ -1087,7 +1092,11 @@ private static ResizedImageGenerationTask getResizedImageGenerationWorkerTask(Im return null; } + @IonosCustomization("Use layout overlay instead of bitmap overlay") public static Bitmap addVideoOverlay(Bitmap thumbnail, Context context) { + if (IonosBuildHelper.isIonosBuild()) { + return thumbnail; + } Drawable playButtonDrawable = ResourcesCompat.getDrawable(MainApp.getAppContext().getResources(), R.drawable.video_white, diff --git a/app/src/main/java/com/owncloud/android/media/MediaControlView.kt b/app/src/main/java/com/owncloud/android/media/MediaControlView.kt index 217c1c153501..28da66d75632 100644 --- a/app/src/main/java/com/owncloud/android/media/MediaControlView.kt +++ b/app/src/main/java/com/owncloud/android/media/MediaControlView.kt @@ -22,11 +22,14 @@ import android.view.LayoutInflater import android.view.View import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo +import android.widget.ImageButton import android.widget.LinearLayout import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.content.ContextCompat import androidx.media3.common.Player +import com.google.android.material.button.MaterialButton +import com.ionos.annotation.IonosCustomization import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.MediaControlBinding @@ -73,6 +76,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : } @Suppress("MagicNumber") + @IonosCustomization("Removed theming") private fun initControllerView() { binding.playBtn.requestFocus() @@ -81,17 +85,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : binding.rewindBtn.setOnClickListener(this) binding.progressBar.run { - viewThemeUtils.platform.themeHorizontalSeekBar(this) setMax(1000) } binding.progressBar.setOnSeekBarChangeListener(this) - - viewThemeUtils.material.run { - colorMaterialButtonPrimaryTonal(binding.rewindBtn) - colorMaterialButtonPrimaryTonal(binding.playBtn) - colorMaterialButtonPrimaryTonal(binding.forwardBtn) - } } /** @@ -222,18 +219,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : } } + @IonosCustomization("ImageButton support") fun updatePausePlay() { - binding.playBtn.icon = ContextCompat.getDrawable( - context, - // use isPlaying instead of playWhenReady - // it represents only the play/pause state - // which is needed to show play/pause icons + val iconResource = if (playerControl?.isPlaying == true) { R.drawable.ic_pause } else { R.drawable.ic_play } - ) + val playBtn: View = binding.playBtn + + if (playBtn is MaterialButton){ + playBtn.icon = ContextCompat.getDrawable( + context, + iconResource + ) + } else if(playBtn is ImageButton){ + playBtn.setImageResource(iconResource) + } + binding.forwardBtn.visibility = if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_FORWARD) == true) { VISIBLE } else { @@ -269,6 +273,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : } @Suppress("MagicNumber") + @IonosCustomization("changed forward to 5 sec") override fun onClick(v: View) { playerControl?.let { playerControl -> val playing = playerControl.playWhenReady @@ -280,7 +285,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : } R.id.rewindBtn -> { - playerControl.seekBack() + val pos = playerControl.currentPosition + playerControl.seekTo(pos - 5000) if (!playing) { playerControl.pause() // necessary in some 2.3.x devices } @@ -288,7 +294,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) : } R.id.forwardBtn -> { - playerControl.seekForward() + val pos = playerControl.currentPosition + playerControl.seekTo(pos + 5000) if (!playing) { playerControl.pause() // necessary in some 2.3.x devices } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index 6c57e9965b23..b539ca1f88ce 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -38,6 +38,7 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; +import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; diff --git a/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java b/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java index 47ecdab72fcd..10b760eccc4d 100644 --- a/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java @@ -26,6 +26,7 @@ import android.os.Binder; import android.text.TextUtils; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.client.core.Clock; import com.nextcloud.client.database.NextcloudDatabase; import com.owncloud.android.R; @@ -35,7 +36,9 @@ import com.owncloud.android.utils.MimeType; import java.util.ArrayList; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import javax.inject.Inject; @@ -81,6 +84,10 @@ public class FileContentProvider extends ContentProvider { private static final String[] PROJECTION_FILE_PATH_AND_OWNER = new String[]{ ProviderTableMeta._ID, ProviderTableMeta.FILE_PATH, ProviderTableMeta.FILE_ACCOUNT_OWNER }; + @IonosCustomization + private static final String[] PROJECTION_PARENT_ID = new String[]{ + ProviderTableMeta._ID, ProviderTableMeta.FILE_PARENT + }; @Inject protected Clock clock; @@ -90,21 +97,28 @@ public class FileContentProvider extends ContentProvider { private UriMatcher mUriMatcher; @Override + @IonosCustomization("Notify parent directories about deletion") public int delete(@NonNull Uri uri, String where, String[] whereArgs) { if (isCallerNotAllowed(uri)) { return -1; } int count; + Set parentIds; SupportSQLiteDatabase db = mDbHelper.getWritableDatabase(); db.beginTransaction(); try { + parentIds = queryParentIds(db, uri, where, whereArgs); count = delete(db, uri, where, whereArgs); db.setTransactionSuccessful(); } finally { db.endTransaction(); } mContext.getContentResolver().notifyChange(uri, null); + for (long parentId : parentIds) { + Uri parentUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, parentId); + mContext.getContentResolver().notifyChange(parentUri, null); + } return count; } @@ -203,6 +217,7 @@ public String getType(@NonNull Uri uri) { } @Override + @IonosCustomization("Notify parent directory when inserting a file") public Uri insert(@NonNull Uri uri, ContentValues values) { if (isCallerNotAllowed(uri)) { return null; @@ -218,6 +233,11 @@ public Uri insert(@NonNull Uri uri, ContentValues values) { db.endTransaction(); } mContext.getContentResolver().notifyChange(newUri, null); + Long parentId = values.getAsLong(ProviderTableMeta.FILE_PARENT); + if (parentId != null) { + Uri parentUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, parentId); + mContext.getContentResolver().notifyChange(parentUri, null); + } return newUri; } @@ -550,21 +570,28 @@ private Cursor query(SupportSQLiteDatabase db, } @Override + @IonosCustomization("Notify parent directories about update") public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (isCallerNotAllowed(uri)) { return -1; } int count; + Set parentIds; SupportSQLiteDatabase db = mDbHelper.getWritableDatabase(); db.beginTransaction(); try { + parentIds = queryParentIds(db, uri, selection, selectionArgs); count = update(db, uri, values, selection, selectionArgs); db.setTransactionSuccessful(); } finally { db.endTransaction(); } mContext.getContentResolver().notifyChange(uri, null); + for (long parentId : parentIds) { + Uri parentUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, parentId); + mContext.getContentResolver().notifyChange(parentUri, null); + } return count; } @@ -595,6 +622,28 @@ private int update(SupportSQLiteDatabase db, Uri uri, ContentValues values, Stri }; } + @IonosCustomization + private Set queryParentIds(SupportSQLiteDatabase db, Uri uri, String where, String... whereArgs) { + Set result = new HashSet<>(); + int uriMatch = mUriMatcher.match(uri); + if (uriMatch == ROOT_DIRECTORY || mUriMatcher.match(uri) == DIRECTORY || mUriMatcher.match(uri) == SINGLE_FILE) { + try (Cursor cursor = query(db, uri, PROJECTION_PARENT_ID, where, whereArgs, null)) { + if (cursor.moveToFirst()) { + do { + int parentIdColumnIndex = cursor.getColumnIndex(ProviderTableMeta.FILE_PARENT); + if (parentIdColumnIndex != -1 && !cursor.isNull(parentIdColumnIndex)) { + long parentId = cursor.getLong(parentIdColumnIndex); + result.add(parentId); + } + } while (cursor.moveToNext()); + } + } catch (Exception e) { + Log_OC.d(TAG, "Error querying parent IDs", e); + } + } + return result; + } + @NonNull @Override public ContentProviderResult[] applyBatch(@NonNull ArrayList operations) diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index ef8df0f9c355..0d98fa45cc63 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -27,6 +27,8 @@ import android.util.Log; import android.widget.Toast; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.owncloud.android.R; @@ -398,6 +400,7 @@ public int update(@NonNull Uri uri, ContentValues values, String selection, Stri @Nullable @Override @SuppressFBWarnings("IOI_USE_OF_FILE_STREAM_CONSTRUCTORS") // TODO remove with API26 + @IonosCustomization public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext()); @@ -418,7 +421,9 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) thr Bitmap avatarBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(avatarKey); - if (avatarBitmap == null) { + if (avatarBitmap == null && IonosBuildHelper.isIonosBuild()) { + avatarBitmap = BitmapUtils.drawableToBitmap(getContext().getDrawable(R.drawable.account_circle_white)); + } else if (avatarBitmap == null) { float avatarRadius = getContext().getResources().getDimension(R.dimen.list_item_avatar_icon_radius); avatarBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar(displayName, avatarRadius)); } diff --git a/app/src/main/java/com/owncloud/android/ui/RoundedCornersConstraintLayout.kt b/app/src/main/java/com/owncloud/android/ui/RoundedCornersConstraintLayout.kt new file mode 100644 index 000000000000..6574db7cdfed --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/RoundedCornersConstraintLayout.kt @@ -0,0 +1,25 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import com.owncloud.android.R + +class RoundedCornersConstraintLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : ConstraintLayout(context, attrs, defStyleAttr) { + + init { + setBackgroundResource(R.drawable.rounded_rect_4dp) + clipToOutline = true + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/SquareFrameLayout.kt b/app/src/main/java/com/owncloud/android/ui/SquareFrameLayout.kt new file mode 100644 index 000000000000..4f12f95adcb0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/SquareFrameLayout.kt @@ -0,0 +1,23 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +class SquareFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/AnimatedDrawerListener.java b/app/src/main/java/com/owncloud/android/ui/activity/AnimatedDrawerListener.java new file mode 100644 index 000000000000..27ee8cb0f8e3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/AnimatedDrawerListener.java @@ -0,0 +1,80 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH.. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui.activity; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.view.View; + +import com.owncloud.android.R; +import com.owncloud.android.utils.theme.ViewThemeUtils; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.drawerlayout.widget.DrawerLayout; + +class AnimatedDrawerListener extends ActionBarDrawerToggle { + private static final float CHANGE_GAIN = 0.1f; + + private final Activity activity; + private final ViewThemeUtils viewThemeUtils; + private final ValueAnimator valueAnimator; + + AnimatedDrawerListener(Activity activity, + DrawerLayout drawerLayout, + @StringRes int openDrawerContentDescRes, + @StringRes int closeDrawerContentDescRes, + ViewThemeUtils viewThemeUtils + ) { + super(activity, drawerLayout, openDrawerContentDescRes, closeDrawerContentDescRes); + + this.activity = activity; + this.viewThemeUtils = viewThemeUtils; + this.valueAnimator = createValueAnimator(); + } + + private ValueAnimator createValueAnimator() { + int colorFrom = this.activity.getResources().getColor(R.color.bg_default); + int colorTo = getOpenedDrawerColor(); + return ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, slideOffset); + + if (shouldUpdateSystemBarColor(slideOffset)) { + this.valueAnimator.setCurrentFraction(slideOffset); + this.viewThemeUtils.platform.themeStatusBar(this.activity, (int) this.valueAnimator.getAnimatedValue()); + } + } + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + + this.viewThemeUtils.platform.themeStatusBar(this.activity, getOpenedDrawerColor()); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + + this.viewThemeUtils.platform.themeStatusBar(this.activity); + } + + private boolean shouldUpdateSystemBarColor(float slideOffset) { + float delta = Math.abs(slideOffset - this.valueAnimator.getAnimatedFraction()); + return delta > CHANGE_GAIN; + } + + private int getOpenedDrawerColor() { + return this.activity.getResources().getColor(R.color.drawer_header_background); + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java index caa83471149a..499caa3593a4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java @@ -11,6 +11,7 @@ import android.content.Intent; import android.os.Bundle; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.util.extensions.AppCompatActivityExtensionsKt; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; @@ -25,6 +26,7 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.status.OCCapability; +import com.owncloud.android.ui.fragment.filesRepository.FilesRepository; import java.util.Optional; @@ -68,8 +70,11 @@ public UserAccountManager getUserAccountManager() { private ClientRepository clientRepository; @Override + @IonosCustomization("Window insets handling, status bar color") protected void onCreate(@Nullable Bundle savedInstanceState) { - AppCompatActivityExtensionsKt.applyEdgeToEdgeWithSystemBarPadding(this); + if(isDefaultWindowInsetsHandlingEnabled()) { + AppCompatActivityExtensionsKt.applyEdgeToEdgeWithSystemBarPadding(this); + } super.onCreate(savedInstanceState); sessionMixin = new SessionMixin(this, accountManager); mixinRegistry.add(sessionMixin); @@ -81,6 +86,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { clientRepository = new RemoteClientRepository(accountManager.getUser(), this, this); } + @IonosCustomization("Window insets handling") + protected boolean isDefaultWindowInsetsHandlingEnabled() { + return true; + } + @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 27340b873e68..bb3670c73feb 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -49,14 +49,15 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.navigation.NavigationView; import com.google.android.material.progressindicator.LinearProgressIndicator; +import com.ionos.annotation.IonosCustomization; +import com.ionos.authorization_method.AuthorizationMethodActivity; +import com.ionos.player.model.PlaybackModel; import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp; import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.files.DeepLinkConstants; import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.common.NextcloudClient; import com.nextcloud.ui.ChooseAccountDialogFragment; @@ -122,6 +123,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; @@ -230,6 +232,9 @@ protected int getMenuItemId() { @Inject protected ClientFactory clientFactory; + @Inject + PlaybackModel playbackModel; + @Override public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { super.onCreate(savedInstanceState, persistentState); @@ -359,6 +364,7 @@ private void exitSelectionMode() { } } + @IonosCustomization private void setupDrawerToggle() { mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open, R.string.drawer_close) { public void onDrawerClosed(View view) { @@ -376,6 +382,10 @@ public void onDrawerClosed(View view) { mDrawerLayout.addDrawerListener(mDrawerToggle); mDrawerToggle.setDrawerIndicatorEnabled(true); mDrawerToggle.setDrawerSlideAnimationEnabled(true); + Drawable drawerIndicator = AppCompatResources.getDrawable(this, R.drawable.ic_menu); + mDrawerToggle.setDrawerArrowDrawable(new SingleStateDrawerArrowDrawable(this, drawerIndicator)); + mDrawerToggle.setHomeAsUpIndicator(R.drawable.ic_arrow_back); + final Drawable backArrow = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_back, null); if (backArrow != null) { viewThemeUtils.platform.tintToolbarArrowDrawable(this, mDrawerToggle, backArrow); @@ -385,12 +395,13 @@ public void onDrawerClosed(View view) { /** * setup quota elements of the drawer. */ + @IonosCustomization("Branded quota progress theming") private void setupQuotaElement() { mQuotaView = (LinearLayout) findQuotaViewById(R.id.drawer_quota); mQuotaProgressBar = (LinearProgressIndicator) findQuotaViewById(R.id.drawer_quota_ProgressBar); mQuotaTextPercentage = (TextView) findQuotaViewById(R.id.drawer_quota_percentage); mQuotaTextLink = (TextView) findQuotaViewById(R.id.drawer_quota_link); - viewThemeUtils.material.colorProgressBar(mQuotaProgressBar, ColorRole.PRIMARY); + viewThemeUtils.material.colorQuotaProgressBar(mQuotaProgressBar); mQuotaProgressBar.setTrackStopIndicatorSize(0); viewThemeUtils.platform.colorViewBackground(mQuotaView); } @@ -554,7 +565,9 @@ private void setDrawerHeaderLogo(Drawable drawable, String serverName) { * * @param navigationView the drawers navigation view */ + @IonosCustomization("Set items tint for correct color") private void setupDrawerMenu(NavigationView navigationView) { + navigationView.setItemIconTintList(null); // setup actions for drawer menu items navigationView.setNavigationItemSelectedListener( @@ -584,6 +597,7 @@ private void filterDrawerMenu(final Menu menu, @NonNull final User user) { DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community, !getResources().getBoolean(R.bool.participate_enabled)); DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !getResources().getBoolean(R.bool.shared_enabled)); DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !getResources().getBoolean(R.bool.show_drawer_logout)); + DrawerMenuUtil.removePersonalFiles(menu); } // region navigation item click @@ -727,16 +741,22 @@ public void openManageAccounts() { startActivityForResult(manageAccountsIntent, ACTION_MANAGE_ACCOUNTS); } + @IonosCustomization public void openAddAccount() { + stopMediaPlayerAndHidePip(); if (MDMConfig.INSTANCE.showIntro(this)) { - Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); - firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); + Intent firstRunIntent = AuthorizationMethodActivity.createInstance(getApplicationContext()); startActivity(firstRunIntent); } else { startAccountCreation(); } } + @IonosCustomization + protected void stopMediaPlayerAndHidePip() { + playbackModel.release(); + } + private void resetFileDepth() { final var ocFileListFragment = getOCFileListFragment(); if (ocFileListFragment != null) { @@ -909,6 +929,7 @@ private void showQuota(boolean showQuota) { * @param relative the percentage of space already used * @param quotaValue {@link GetUserInfoRemoteOperation#SPACE_UNLIMITED} or other to determinate state */ + @IonosCustomization private void setQuotaInformation(long usedSpace, long totalSpace, int relative, long quotaValue) { if (GetUserInfoRemoteOperation.SPACE_UNLIMITED == quotaValue) { mQuotaTextPercentage.setText(String.format( @@ -924,12 +945,9 @@ private void setQuotaInformation(long usedSpace, long totalSpace, int relative, mQuotaProgressBar.setProgress(relative); if (relative < RELATIVE_THRESHOLD_WARNING) { - viewThemeUtils.material.colorProgressBar(mQuotaProgressBar, ColorRole.PRIMARY); + mQuotaProgressBar.setIndicatorColor(getResources().getColor(R.color.sidemenu_progress_bar_color, getTheme())); } else { - viewThemeUtils.material.colorProgressBar( - mQuotaProgressBar, - getResources().getColor(R.color.infolevel_warning, null) - ); + mQuotaProgressBar.setIndicatorColor(getResources().getColor(R.color.sidemenu_warn_progress_bar_color, getTheme())); } updateQuotaLink(); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java index fe1107d1b140..52a6f0a91fc1 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java @@ -22,6 +22,7 @@ import android.webkit.WebView; import android.widget.ProgressBar; +import com.ionos.annotation.IonosCustomization; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.ExternalsiteWebviewBinding; @@ -54,6 +55,7 @@ public class ExternalSiteWebView extends FileActivity { String url; @Override + @IonosCustomization protected final void onCreate(Bundle savedInstanceState) { Log_OC.v(TAG, "onCreate() start"); bindView(); @@ -78,6 +80,8 @@ protected final void onCreate(Bundle savedInstanceState) { setContentView(getRootView()); postOnCreate(); + + viewThemeUtils.platform.themeStatusBar(this); } protected void postOnCreate() { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 0553977eea6b..002bba7cd5bf 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -30,6 +30,8 @@ import android.text.TextUtils; import com.google.android.material.snackbar.Snackbar; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.jobs.BackgroundJobManager; @@ -473,7 +475,14 @@ protected void requestCredentialsUpdate(Account account) { new CheckRemoteWipeTask(backgroundJobManager, account, new WeakReference<>(this)).execute(); } + @IonosCustomization("Remove account with invalid token on Ionos build") public void performCredentialsUpdate(Account account, Context context) { + if (IonosBuildHelper.isIonosBuild()) { + /// Remove account and allow SessionMixin to handle switching to another account + /// or requesting new account creation + backgroundJobManager.startAccountRemovalJob(account.name, false); + return; + } try { /// step 1 - invalidate credentials of current account OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 29ba6415519c..86927f554ddc 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -11,6 +11,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2012-2013 David A. Velasco * SPDX-FileCopyrightText: 2011 Bartosz Przybylski + * SPDX-FileCopyrightText: 2026 STRATO GmbH. * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity @@ -54,6 +55,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.ionos.annotation.IonosCustomization +import com.ionos.player.ui.PlayerLauncher import com.nextcloud.android.common.core.utils.ecosystem.AccountReceiverCallback import com.nextcloud.appReview.InAppReviewHelper import com.nextcloud.client.account.User @@ -254,6 +257,9 @@ class FileDisplayActivity : @Inject lateinit var syncedFolderProvider: SyncedFolderProvider + @Inject + lateinit var playerLauncher: PlayerLauncher + @Inject lateinit var passCodeManager: PassCodeManager @@ -851,6 +857,7 @@ class FileDisplayActivity : } } + @IonosCustomization("Video in custom player") private fun tryStartWaitingPreview(success: Boolean): Boolean { if (!success) return false @@ -858,11 +865,6 @@ class FileDisplayActivity : val file = mWaitingToPreview ?: return false return when { - MimeTypeUtil.isVideo(file) -> { - startImagePreview(file, true) - true - } - PreviewMediaActivity.canBePreviewed(file) -> { startMediaPreview(file, 0, true, true, true, true) true @@ -2021,6 +2023,7 @@ class FileDisplayActivity : startImagePreview(file, file.isDown, type) } + @IonosCustomization("Video in custom player") fun previewFile(file: OCFile, setFabVisible: CompletionCallback?) { if (!file.isDown) { Log_OC.d(TAG, "File is not downloaded, cannot be previewed") @@ -2034,9 +2037,6 @@ class FileDisplayActivity : } else if (PreviewTextFileFragment.canBePreviewed(file)) { setFabVisible?.onComplete(false) startTextPreview(file, false) - } else if (MimeTypeUtil.isVideo(file)) { - setFabVisible?.onComplete(false) - startImagePreview(file, true) } else if (PreviewMediaActivity.Companion.canBePreviewed(file)) { setFabVisible?.onComplete(false) startMediaPreview(file, 0, true, true, false, true) @@ -2700,18 +2700,10 @@ class FileDisplayActivity : } } + @IonosCustomization("Launch of custom player") private fun startMediaActivity(file: OCFile?, startPlaybackPosition: Long, autoplay: Boolean, user: User?) { - val previewMediaIntent = Intent(this, PreviewMediaActivity::class.java) - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_FILE, file) - - // Safely handle the absence of a user - if (user != null) { - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_USER, user) - } - - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_START_POSITION, startPlaybackPosition) - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_AUTOPLAY, autoplay) - startActivity(previewMediaIntent) + val searchType = listOfFilesFragment?.currentSearchType + playerLauncher.launch(this, file!!, searchType) } fun configureToolbarForPreview(file: OCFile?) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index b0b5c69e0b7a..84197f9cff08 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -21,8 +21,11 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.OnBackPressedCallback +import androidx.appcompat.content.res.AppCompatResources import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.appbar.MaterialToolbar +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.utils.extensions.getParcelableArgument @@ -71,6 +74,8 @@ open class FolderPickerActivity : private var captionText: String? = null + private var showsBrandCaption = false + private var action: String? = null private var targetFilePaths: ArrayList? = null @@ -109,6 +114,7 @@ open class FolderPickerActivity : } updateActionBarTitleAndHomeButtonByString(captionText) + updateToolbarBrandLogo() handleBackPress() } @@ -131,6 +137,7 @@ open class FolderPickerActivity : setupUIForChooseButton() } else { captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) + showsBrandCaption = true } } @@ -445,6 +452,24 @@ open class FolderPickerActivity : getSelectedFolderPathTitle()?.let { viewThemeUtils.files.themeActionBar(this, actionBar, it) } + updateToolbarBrandLogo() + } + } + + @IonosCustomization("Brand logo instead of the root folder caption") + private fun updateToolbarBrandLogo() { + if (!showsBrandCaption || !resources.getBoolean(R.bool.show_brand_logo_in_folder_picker)) { + return + } + + val folder = currentFolder + val atRoot = (folder == null || folder.parentId == 0L) + + supportActionBar?.setDisplayShowTitleEnabled(!atRoot) + findViewById(R.id.toolbar)?.logo = if (atRoot) { + AppCompatResources.getDrawable(this, R.drawable.nextcloud_logo) + } else { + null } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt index 47de3ea71b76..4f843f05a781 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt @@ -32,6 +32,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.common.collect.Sets +import com.ionos.annotation.IonosCustomization +import com.ionos.authorization_method.AuthorizationMethodActivity import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.download.FileDownloadEventBroadcaster @@ -239,16 +241,17 @@ class ManageAccountsActivity : return result } + @IonosCustomization override fun showFirstRunActivity() { - val intent = Intent(applicationContext, FirstRunActivity::class.java).apply { - putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true) - } + stopMediaPlayerAndHidePip() + val intent = AuthorizationMethodActivity.createInstance(applicationContext); startActivity(intent) } @Suppress("TooGenericExceptionCaught") @SuppressLint("NotifyDataSetChanged") override fun startAccountCreation() { + stopMediaPlayerAndHidePip() val am = AccountManager.get(applicationContext) am.addAccount( MainApp.getAccountType(this), diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 48c4e10a6a00..38f7f815da52 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -48,6 +48,9 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.ionos.annotation.IonosCustomization; +import com.ionos.privacy.DataProtectionActivity; +import com.ionos.privacy.PrivacyPreferences; import com.nextcloud.client.account.User; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.jobs.upload.FileUploadHelper; @@ -144,6 +147,7 @@ public class ReceiveExternalFilesActivity extends FileActivity public static final int SINGLE_PARENT = 1; @Inject AppPreferences preferences; + @Inject PrivacyPreferences privacyPreferences; @Inject LocalBroadcastManager localBroadcastManager; @Inject SyncedFolderProvider syncedFolderProvider; @@ -179,6 +183,7 @@ public class ReceiveExternalFilesActivity extends FileActivity private final ExecutorService executorService = Executors.newSingleThreadExecutor(); @Override + @IonosCustomization protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { String parentPath = savedInstanceState.getString(KEY_PARENTS); @@ -192,6 +197,14 @@ protected void onCreate(Bundle savedInstanceState) { mAccountManager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE); super.onCreate(savedInstanceState); + + String accountName = accountManager.getCurrentOwnCloudAccount() != null + ? accountManager.getCurrentOwnCloudAccount().getName() + : null; + if (!privacyPreferences.isDataProtectionProcessed(accountName)) { + startActivity(DataProtectionActivity.createIntent(this)); + } + binding = ReceiveExternalFilesBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); @@ -303,9 +316,10 @@ protected void onDestroy() { } @Override + @IonosCustomization public void onSortingOrderChosen(FileSortOrder newSortOrder) { preferences.setSortOrder(mFile, newSortOrder); - sortButton.setText(DisplayUtils.getSortOrderStringId(newSortOrder)); + sortButton.setIconResource(DisplayUtils.getSortOrderIconRes(newSortOrder)); populateDirectoryList(null); } @@ -748,6 +762,7 @@ private void setupActionBarSubtitle() { } } + @IonosCustomization private void populateDirectoryList(OCFile file) { setupEmptyList(); setupToolbar(); @@ -816,7 +831,7 @@ private void populateDirectoryList(OCFile file) { sortButton = binding.toolbarLayout.sortButton; FileSortOrder sortOrder = preferences.getSortOrderByFolder(mFile); - sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)); + sortButton.setIconResource(DisplayUtils.getSortOrderIconRes(sortOrder)); sortButton.setOnClickListener(l -> openSortingOrderDialogFragment(getSupportFragmentManager(), sortOrder)); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index f5789afd411f..e967d5020ea1 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -17,6 +17,7 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; @@ -24,6 +25,7 @@ import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -39,9 +41,14 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.webkit.URLUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.ionos.annotation.IonosCustomization; +import com.ionos.privacy.PrivacySettingsActivity; +import com.ionos.utils.IonosBuildHelper; +import com.nextcloud.android.common.ui.util.extensions.WindowExtensionsKt; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; @@ -150,7 +157,19 @@ public class SettingsActivity extends PreferenceActivity @SuppressWarnings("deprecation") @Override + @IonosCustomization("Delegate fix") public void onCreate(Bundle savedInstanceState) { + + boolean isApiLevel35OrHigher = (Build.VERSION.SDK_INT >= 35); + if (isApiLevel35OrHigher) { + final var window = getWindow(); + if (window != null) { + WindowExtensionsKt.addSystemBarPaddings(getWindow()); + final var flag = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + window.setFlags(flag, flag); + } + } + super.onCreate(savedInstanceState); getDelegate().installViewFactory(); @@ -351,10 +370,11 @@ private void setupSyncCategory() { viewThemeUtils.files.themePreferenceCategory(preferenceCategorySync); setupAutoUploadPreference(preferenceCategorySync); - setupInternalTwoWaySyncPreference(); + setupInternalTwoWaySyncPreference(preferenceCategorySync); setupAllFilesAccessPreference(preferenceCategorySync); } + @IonosCustomization private void setupMoreCategory() { final PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more"); viewThemeUtils.files.themePreferenceCategory(preferenceCategoryMore); @@ -373,6 +393,8 @@ private void setupMoreCategory() { removeE2EFilesAndKeys(preferenceCategoryMore); + setupPrivacySettingsPreference(preferenceCategoryMore); + setupHelpPreference(preferenceCategoryMore); setupRecommendPreference(preferenceCategoryMore); @@ -421,6 +443,7 @@ private void setupLoggingPreference(PreferenceCategory preferenceCategoryMore) { } + @IonosCustomization private void setupRecommendPreference(PreferenceCategory preferenceCategoryMore) { boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled); Preference pRecommend = findPreference("recommend"); @@ -435,8 +458,13 @@ private void setupRecommendPreference(PreferenceCategory preferenceCategoryMore) String appName = getString(R.string.app_name); String downloadUrlGooglePlayStore = getString(R.string.url_app_download); String downloadUrlFDroid = getString(R.string.fdroid_link); - String downloadUrls = String.format(getString(R.string.recommend_urls), - downloadUrlGooglePlayStore, downloadUrlFDroid); + String downloadUrls; + if (URLUtil.isValidUrl(downloadUrlFDroid)) { + downloadUrls = String.format(getString(R.string.recommend_urls), + downloadUrlGooglePlayStore, downloadUrlFDroid); + } else { + downloadUrls = downloadUrlGooglePlayStore; + } String recommendSubject = String.format(getString(R.string.recommend_subject), appName); String recommendText = String.format(getString(R.string.recommend_text), @@ -604,6 +632,16 @@ private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore, .show(); } + private void setupPrivacySettingsPreference(PreferenceCategory preferenceCategoryMore) { + Preference privacySettings = findPreference("privacy_settings"); + if (privacySettings != null) { + privacySettings.setOnPreferenceClickListener(preference -> { + startActivity(PrivacySettingsActivity.createIntent(this)); + return true; + }); + } + } + private void setupHelpPreference(PreferenceCategory preferenceCategoryMore) { boolean helpEnabled = getResources().getBoolean(R.bool.help_enabled); Preference pHelp = findPreference("help"); @@ -632,9 +670,15 @@ private void setupAutoUploadPreference(PreferenceCategory preferenceCategoryMore } } - private void setupInternalTwoWaySyncPreference() { + @IonosCustomization("internal_two_way_sync was hidden") + private void setupInternalTwoWaySyncPreference(PreferenceCategory preferenceCategorySync) { Preference twoWaySync = findPreference("internal_two_way_sync"); + if (IonosBuildHelper.isIonosBuild()) { + preferenceCategorySync.removePreference(twoWaySync); + return; + } + twoWaySync.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(this, InternalTwoWaySyncActivity.class); startActivity(intent); @@ -886,6 +930,7 @@ private void disableLock(String lock) { } } + @IonosCustomization("Workaround to hide prefStoragePath") private void setupGeneralCategory() { final PreferenceCategory preferenceCategoryGeneral = (PreferenceCategory) findPreference("general"); viewThemeUtils.files.themePreferenceCategory(preferenceCategoryGeneral); @@ -894,6 +939,7 @@ private void setupGeneralCategory() { prefDataLoc = findPreference(AppPreferencesImpl.DATA_STORAGE_LOCATION); if (prefDataLoc != null) { + preferenceCategoryGeneral.removePreference(prefDataLoc); prefDataLoc.setOnPreferenceClickListener(p -> { Intent intent = ExtendedSettingsActivity.Companion.createIntent(this, ExtendedSettingsActivityDialog.StorageLocation); startActivityForResult(intent, ExtendedSettingsActivityDialog.StorageLocation.getResultId()); @@ -937,6 +983,12 @@ private void setListBackground() { getListView().setBackgroundColor(ContextCompat.getColor(this, R.color.bg_default)); } + @IonosCustomization + private void setListPadding() { + int bottom = (int) getResources().getDimension(R.dimen.settings_screen_list_bottom_padding); + getListView().setPadding(0, 0, 0, bottom); + } + private String getAppVersion() { String temp; try { @@ -1197,6 +1249,11 @@ public void invalidateOptionsMenu() { getDelegate().invalidateOptionsMenu(); } + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(getDelegate().attachBaseContext2(newBase)); + } + private AppCompatDelegate getDelegate() { if (delegate == null) { delegate = AppCompatDelegate.create(this, null); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.kt index d03e13f708a6..70445f028ae8 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.kt @@ -14,7 +14,9 @@ package com.owncloud.android.ui.activity import android.os.Bundle +import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable @@ -50,12 +52,18 @@ class ShareActivity : @Inject lateinit var overlayManager: OverlayManager + @IonosCustomization("Toolbar instead of dialog if available") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ShareActivityBinding.inflate(layoutInflater) setContentView(binding.root) - configureDialogWindow() + + findViewById(R.id.toolbar)?.let { toolbar -> + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { finish() } + } ?: configureDialogWindow() val currentUser = user.orElse(null) ?: run { finish() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SingleStateDrawerArrowDrawable.kt b/app/src/main/java/com/owncloud/android/ui/activity/SingleStateDrawerArrowDrawable.kt new file mode 100644 index 000000000000..60ed8da431ed --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/SingleStateDrawerArrowDrawable.kt @@ -0,0 +1,24 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui.activity + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable + +class SingleStateDrawerArrowDrawable( + context: Context, + private val drawable: Drawable, +) : DrawerArrowDrawable(context) { + + override fun draw(canvas: Canvas) { + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 662b879f0bce..0b31c85d8563 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService @@ -860,6 +861,8 @@ class SyncedFoldersActivity : item.setExcludeHidden(excludeHidden) } + @IonosCustomization("StackOverflow fix") + private var externalStoragePermissionRequestCount = 0 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index 7db6c88e12bb..dffdcf20f22b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -19,6 +19,7 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -30,6 +31,7 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.textview.MaterialTextView; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; @@ -64,8 +66,8 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable private AppBarLayout mAppBar; private RelativeLayout mDefaultToolbar; private MaterialToolbar mToolbar; - private MaterialCardView mHomeSearchContainer; - private LinearLayout mHomeSearchToolbar; + //private MaterialCardView mHomeSearchContainer; + private ViewGroup mHomeSearchToolbar; private ImageView mPreviewImage; private FrameLayout mPreviewImageContainer; private LinearLayout mInfoBox; @@ -82,6 +84,7 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable * Toolbar setup that must be called in implementer's {@link #onCreate} after {@link #setContentView} if they want * to use the toolbar. */ + @IonosCustomization("search container, menu btn") private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListButtonGroup) { mToolbar = findViewById(R.id.toolbar); setSupportActionBar(mToolbar); @@ -89,7 +92,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mAppBar = findViewById(R.id.appbar); mDefaultToolbar = findViewById(R.id.default_toolbar); mHomeSearchToolbar = findViewById(R.id.home_toolbar); - mHomeSearchContainer = findViewById(R.id.home_search_container); + //mHomeSearchContainer = findViewById(R.id.home_search_container); mMenuButton = findViewById(R.id.menu_button); mSearchText = findViewById(R.id.search_text); mSwitchAccountButton = findViewById(R.id.switch_account_button); @@ -110,13 +113,10 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mToolbarSpinner = findViewById(R.id.toolbar_spinner); - viewThemeUtils.material.themeToolbar(mToolbar); - viewThemeUtils.material.colorToolbarOverflowIcon(mToolbar); viewThemeUtils.platform.themeStatusBar(this); viewThemeUtils.material.colorMaterialTextButton(mSwitchAccountButton); - viewThemeUtils.material.themeSearchCardView(mHomeSearchContainer); - viewThemeUtils.material.colorMaterialButtonContent(mMenuButton, ColorRole.ON_SURFACE); + //viewThemeUtils.material.themeSearchCardView(mHomeSearchContainer); viewThemeUtils.material.colorMaterialButtonContent(mNotificationButton, ColorRole.ON_SURFACE); viewThemeUtils.platform.colorTextView(mSearchText, ColorRole.ON_SURFACE_VARIANT); } @@ -215,6 +215,7 @@ private String getActionBarTitleFromFile(OCFile file) { return fileDataStorageManager.getFilenameConsideringOfflineOperation(file); } + @IonosCustomization("Search hint") protected void updateActionBarTitleAndHomeButton(OCFile file) { final OCFileDepth currentDirDepth = getCurrentDirDepth(); final boolean isRoot = isRoot(file) || currentDirDepth == OCFileDepth.Root; @@ -230,7 +231,7 @@ protected void updateActionBarTitleAndHomeButton(OCFile file) { showHomeSearchToolbar(canShowSearchBar); if (mSearchText != null) { - mSearchText.setText(getString(R.string.appbar_search_in, title)); + mSearchText.setText(getString(R.string.actionbar_search, title)); } final var actionBar = getSupportActionBar(); @@ -267,11 +268,11 @@ public void hideSearchView(OCFile chosenFile) { } @SuppressLint("PrivateResource") + @IonosCustomization private void showHomeSearchToolbar(boolean isShow) { if (mAppBar == null) { return; } - viewThemeUtils.material.themeToolbar(mToolbar); if (isShow) { @@ -280,8 +281,6 @@ private void showHomeSearchToolbar(boolean isShow) { R.animator.appbar_elevation_off)); mDefaultToolbar.setVisibility(View.GONE); mHomeSearchToolbar.setVisibility(View.VISIBLE); - viewThemeUtils.material.themeSearchCardView(mHomeSearchContainer); - viewThemeUtils.material.themeSearchBarText(mSearchText); } else { mAppBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(mAppBar.getContext(), R.animator.appbar_elevation_on)); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index 5dab3516d444..e6e91d4fca71 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -27,6 +27,7 @@ import android.widget.ArrayAdapter; import android.widget.TextView; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.core.Clock; @@ -143,6 +144,7 @@ public static void startUploadActivityForResult(Activity activity, @Override @SuppressLint("WrongViewCast") // wrong error on finding local_files_list + @IonosCustomization public void onCreate(Bundle savedInstanceState) { Log_OC.d(TAG, "onCreate() start"); super.onCreate(savedInstanceState); @@ -178,7 +180,7 @@ public void onCreate(Bundle savedInstanceState) { /// USER INTERFACE // Drop-down navigation - mDirectories = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item); + mDirectories = new ArrayAdapter<>(this, R.layout.simple_spinner_item); mDirectories.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); fillDirectoryDropdown(); @@ -282,6 +284,7 @@ private void fillDirectoryDropdown() { } @Override + @IonosCustomization public boolean onCreateOptionsMenu(Menu menu) { mOptionsMenu = menu; getMenuInflater().inflate(R.menu.activity_upload_files, menu); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index b6ef36356a6b..f66563868b63 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -21,6 +21,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService @@ -139,6 +140,7 @@ class UploadListActivity : swipeListRefreshLayout?.setOnRefreshListener { this.refresh() } } + @IonosCustomization("Remove empty list icon alpha") private fun setupEmptyList() { binding?.run { list.setEmptyView(emptyList.getRoot()) @@ -147,8 +149,6 @@ class UploadListActivity : emptyListIcon.run { setImageResource(R.drawable.uploads) - getDrawable().mutate() - setAlpha(0.5f) setVisibility(View.VISIBLE) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java index 9743507c85f2..2139a0f4776b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java @@ -24,6 +24,7 @@ import android.webkit.URLUtil; import android.widget.ImageView; +import com.ionos.annotation.IonosCustomization; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; @@ -245,8 +246,9 @@ public void onLoadCleared(@Nullable Drawable placeholder) { }; } - + @IonosCustomization("Hide account id") private void populateUserInfoUi(UserInfo userInfo) { + binding.userinfoUsername.setVisibility(View.GONE); binding.userinfoUsername.setText(user.getAccountName()); binding.userinfoIcon.setTag(user.getAccountName()); DisplayUtils.setAvatar(user, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt index 7633297ce066..51af7915c401 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt @@ -10,6 +10,8 @@ package com.owncloud.android.ui.adapter import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter +import com.ionos.annotation.IonosCustomization +import com.ionos.utils.IonosBuildHelper import com.nextcloud.client.account.User import com.nextcloud.ui.fileInfo.FileInfoFragment import com.owncloud.android.datamodel.OCFile @@ -35,17 +37,30 @@ class FileDetailTabAdapter( var fileDetailActivitiesFragment: FileDetailActivitiesFragment? = null private set - override fun createFragment(position: Int): Fragment = when (position) { - Tab.Sharing.position -> FileDetailSharingFragment.newInstance(file, user) - .also { fileDetailSharingFragment = it } + @IonosCustomization("Hide tabs in IONOS") + override fun createFragment(position: Int): Fragment { + if (IonosBuildHelper.isIonosBuild()) { + return FileDetailSharingFragment.newInstance(file, user) + .also { fileDetailSharingFragment = it } + } + + return when (position) { + Tab.Sharing.position -> FileDetailSharingFragment.newInstance(file, user) + .also { fileDetailSharingFragment = it } - Tab.Details.position -> FileInfoFragment.newInstance(file, user) + Tab.Details.position -> FileInfoFragment.newInstance(file, user) - else -> FileDetailActivitiesFragment.newInstance(file, user) - .also { fileDetailActivitiesFragment = it } + else -> FileDetailActivitiesFragment.newInstance(file, user) + .also { fileDetailActivitiesFragment = it } + } } + @IonosCustomization("Hide tabs in IONOS") override fun getItemCount(): Int { + if (IonosBuildHelper.isIonosBuild()) { + return if (showSharingTab) 1 else 0 + } + // always show Activities var count = 1 diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index fead84f34822..1f3a7f2cfeb3 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.get import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.extensions.makeRounded @@ -33,7 +34,7 @@ class GalleryRowHolder( private val defaultThumbnailSize: Float, private val ocFileListDelegate: OCFileListDelegate, val storageManager: FileDataStorageManager, - galleryAdapter: GalleryAdapter, + private val galleryAdapter: GalleryAdapter, private val viewThemeUtils: ViewThemeUtils ) : SectionedViewHolder(binding.root) { val context = galleryAdapter.context @@ -46,11 +47,12 @@ class GalleryRowHolder( private val iconRadius by lazy { context.resources.getDimension(R.dimen.activity_icon_radius) } private val standardMargin by lazy { context.resources.getDimension(R.dimen.standard_margin) } private val checkBoxMargin by lazy { context.resources.getDimension(R.dimen.standard_quarter_padding) } + @IonosCustomization("space between photos") + private val photoSpacing by lazy { context.resources.getDimensionPixelSize(R.dimen.ionos_media_items_padding) } + @IonosCustomization("checkbox icon") private val checkedDrawable by lazy { - ContextCompat.getDrawable(context, R.drawable.ic_checkbox_marked)?.also { - viewThemeUtils.platform.tintDrawable(context, it, ColorRole.PRIMARY) - } + ContextCompat.getDrawable(context, R.drawable.ic_checkbox_marked) } private val uncheckedDrawable by lazy { @@ -119,39 +121,16 @@ class GalleryRowHolder( } } + @IonosCustomization("squared cell size depending on screen width and column count") private fun getDimensions(row: GalleryRow): List> { - val screenWidthPx = context.resources.displayMetrics.widthPixels.toFloat() - val marginPx = smallMargin.toFloat() - val totalMargins = marginPx * (row.files.size - 1) - val availableWidth = screenWidthPx - totalMargins - - val aspectRatios = row.files.map { file -> - val (w, h) = OCFileUtils.getImageSize(file, defaultThumbnailSize) - if (h > 0) w.toFloat() / h else 1.0f - } - - val sumAspectRatios = aspectRatios.sum() - - // calculate row height based on aspect ratios - val rowHeightFloat = if (sumAspectRatios > 0) availableWidth / sumAspectRatios else defaultThumbnailSize - val finalHeight = rowHeightFloat.toInt() - - // for each aspect ratio calculate widths - val finalWidths = aspectRatios.map { ratio -> (rowHeightFloat * ratio).toInt() }.toMutableList() - val usedWidth = finalWidths.sum() - - // based on screen width get remaining pixels - val remainingPixels = (availableWidth - usedWidth).toInt() - - // add to remaining pixels to last image - if (remainingPixels > 0 && finalWidths.isNotEmpty()) { - val lastIndex = finalWidths.lastIndex - finalWidths[lastIndex] = finalWidths[lastIndex] + remainingPixels - } - - return finalWidths.map { w -> w to finalHeight } + val count = row.files.size + val screenWidthPx = context.resources.displayMetrics.widthPixels + val totalMargins = photoSpacing * galleryAdapter.columns + val cellSize = (screenWidthPx - totalMargins) / galleryAdapter.columns + return List(count) { cellSize to cellSize } } + @IonosCustomization("spacing and marging adjustments") private fun adjustFile(index: Int, file: OCFile, dims: Pair, row: GalleryRow) { val (width, height) = dims val frameLayout = binding.rowLayout[index] as FrameLayout @@ -165,9 +144,12 @@ class GalleryRowHolder( ocFileListDelegate.bindGalleryRow(shimmer, thumbnail, file, this, dims) - val endMargin = if (index < row.files.size - 1) smallMargin else zero + val halfSpacing = photoSpacing / 2 + val startMargin = if (index == 0) halfSpacing else zero + val endMargin = if (index < row.files.size - 1) photoSpacing else halfSpacing + thumbnail.layoutParams = FrameLayout.LayoutParams(width, height).apply { - setMargins(0, 0, endMargin, smallMargin) + setMargins(startMargin, 0, endMargin, photoSpacing) } shimmer.layoutParams = FrameLayout.LayoutParams(width, height) frameLayout.requestLayout() diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt index dd6a506c1036..606055e073be 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt @@ -18,6 +18,7 @@ import android.text.TextUtils import android.view.View import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView +import com.ionos.annotation.IonosCustomization import com.nextcloud.utils.extensions.remainingDownloadLimit import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R @@ -121,6 +122,7 @@ internal class LinkShareViewHolder(itemView: View) : RecyclerView.ViewHolder(ite binding.subline.visibility = View.GONE } + @IonosCustomization("colorPrimaryTextViewElement") private fun setPermissionName( binding: FileDetailsShareLinkShareItemBinding?, context: Context?, @@ -139,10 +141,10 @@ internal class LinkShareViewHolder(itemView: View) : RecyclerView.ViewHolder(ite binding.permissionName.visibility = View.VISIBLE binding.permissionName.text = permissionName } - - viewThemeUtils?.androidx?.colorPrimaryTextViewElement(binding.permissionName) + //viewThemeUtils?.androidx?.colorPrimaryTextViewElement(binding.permissionName) } + @IonosCustomization("colorImageViewBackgroundAndIcon") private fun setOnClickListeners( binding: FileDetailsShareLinkShareItemBinding?, listener: ShareeListAdapterListener, @@ -152,7 +154,7 @@ internal class LinkShareViewHolder(itemView: View) : RecyclerView.ViewHolder(ite return } - viewThemeUtils?.platform?.colorImageViewBackgroundAndIcon(binding.icon) + //viewThemeUtils?.platform?.colorImageViewBackgroundAndIcon(binding.icon) binding.overflowMenu.setOnClickListener { listener.showSharingMenuActionSheet(publicShare) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt index bbbfc9ade682..4302e884f4d0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt @@ -8,8 +8,12 @@ package com.owncloud.android.ui.adapter import android.widget.TextView +import com.ionos.annotation.IonosCustomization +import com.ionos.player.ui.common.PlayerProgressIndicator internal interface ListGridItemViewHolder : ListViewHolder { val fileName: TextView + @IonosCustomization("Show current playback progress") + val playerProgressIndicator: PlayerProgressIndicator val extension: TextView? } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java index 72208f34f20c..4409344b4ab4 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java @@ -23,6 +23,7 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.utils.FileHelper; @@ -204,6 +205,7 @@ private boolean shouldShowHeader() { } @Override + @IonosCustomization("ic_checkbox_marked") public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { boolean headerVisible = shouldShowHeader(); int headerOffset = headerVisible ? 1 : 0; @@ -244,9 +246,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi grid.itemLayout.setBackgroundColor( ContextCompat.getColor(mContext, R.color.selected_item_background) ); - grid.checkbox.setImageDrawable( - viewThemeUtils.platform.tintDrawable(mContext, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY) - ); + grid.checkbox.setImageResource(R.drawable.ic_checkbox_marked); } else { grid.itemLayout.setBackgroundColor( mContext.getResources().getColor(R.color.bg_default) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index e68ba687ec1f..d615801edd6a 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -27,6 +27,8 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.google.android.material.chip.Chip; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; @@ -367,6 +369,7 @@ public OCFile getItem(int position) { } @Override + @IonosCustomization public int getItemViewType(int position) { if (shouldShowHeader() && position == 0) { return VIEW_TYPE_HEADER; @@ -377,6 +380,10 @@ public int getItemViewType(int position) { return VIEW_TYPE_FOOTER; } + if (IonosBuildHelper.isIonosBuild()) { + return VIEW_TYPE_ITEM; + } + OCFile item = getItem(position); if (item == null) { return VIEW_TYPE_ITEM; @@ -460,7 +467,8 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (shouldShowOpenInNotes()) { final var listHeaderOpenInBinding = headerBinding.openIn; - viewThemeUtils.files.themeFilledCardView(listHeaderOpenInBinding.infoCard); + //TODO + //viewThemeUtils.files.themeFilledCardView(listHeaderOpenInBinding.infoCard); listHeaderOpenInBinding.infoText.setText(String.format(activity.getString(R.string.folder_best_viewed_in), activity.getString(R.string.ecosystem_apps_notes))); @@ -552,8 +560,10 @@ private void setFilenameAndExtension(ListGridItemViewHolder holder, OCFile file) } else { handleListMode(holder, pair, isFolder); } + holder.getPlayerProgressIndicator().setFile(file); } - + + @IonosCustomization("Custom grid view, Show current playback progress") private void handleGridMode(String filename, OCFileListGridItemViewHolder holder, Pair filenamePair, OCFile file) { boolean containsBidiControlCharacters = FileStorageUtils.containsBidiControlCharacters(filename); ViewExtensionsKt.setVisibleIf(holder.getFileName(),!containsBidiControlCharacters); @@ -591,24 +601,27 @@ private void handleListMode(ListGridItemViewHolder holder, } } + @IonosCustomization("Change share icon") private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { final var sharedAvatars = holder.getSharedAvatars(); + sharedAvatars.setVisibility(View.GONE); + if (sharedAvatars.getChildCount() > 0) { + sharedAvatars.removeAllViews(); + } if (!(file.isSharedWithMe() || file.isSharedWithSharee()) || isMultiSelect() || gridView || hideItemOptions) { - sharedAvatars.setVisibility(View.GONE); - if (sharedAvatars.getChildCount() > 0) { - sharedAvatars.removeAllViews(); - } return; } - sharedAvatars.setVisibility(View.VISIBLE); - if (sharedAvatars.getChildCount() > 0) { - sharedAvatars.removeAllViews(); + if (file.isSharedViaLink()) { + holder.getShared().setImageResource(R.drawable.ic_shared_all_types); + } else { + holder.getShared().setImageResource(R.drawable.shared_via_users); } + holder.getShared().setVisibility(View.VISIBLE); final var avatars = helper.getAvatarSharees(file, userId); sharedAvatars.setAvatars(user, avatars, viewThemeUtils); - sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + holder.getShared().setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); } private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { @@ -795,6 +808,7 @@ private String generateFooterText(int filesCount, int foldersCount) { return output; } + @IonosCustomization("Hide header in IONOS") public boolean shouldShowHeader() { if (currentDirectory == null) { return false; @@ -816,7 +830,7 @@ public boolean shouldShowHeader() { return false; } - return !TextUtils.isEmpty(currentDirectory.getRichWorkspace().trim()); + return !IonosBuildHelper.isIonosBuild() && !TextUtils.isEmpty(currentDirectory.getRichWorkspace().trim()); } /** diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index 3f607b3e6b0b..34fd3648b8d2 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -8,10 +8,14 @@ package com.owncloud.android.ui.adapter import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.LayerDrawable import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.jobs.download.FileDownloadHelper @@ -45,6 +49,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.job import kotlinx.coroutines.launch +import androidx.core.graphics.drawable.toDrawable @Suppress("LongParameterList", "TooManyFunctions") class OCFileListDelegate( @@ -124,7 +129,8 @@ class OCFileListDelegate( } imageView.setImageBitmap(overlay) } else { - imageView.setImageDrawable(OCFileUtils.getMediaPlaceholder(file, imageDimension)) + @IonosCustomization("Custom placeholder for gallery images while loading") + imageView.setImageDrawable(Color.LTGRAY.toDrawable()) } val job = ioScope.launch { @@ -148,10 +154,21 @@ class OCFileListDelegate( } } + @IonosCustomization("image post processing - custom placeholder for failed image loading") override fun onError() { if (imageView.tag == file.fileId) { Log_OC.d(tag, "setGalleryImage.onError()") DisplayUtils.stopShimmer(shimmer, imageView) + imageView.post { + imageView.scaleType = ImageView.ScaleType.FIT_CENTER + val inset = (imageDimension.first * 0.45f).toInt() + val icon = OCFileUtils.getMediaPlaceholder(file, imageDimension) + val backgroundColor = context.getColor(R.color.ionos_gallery_item_background_color) + val layered = LayerDrawable(arrayOf(backgroundColor.toDrawable(), icon)).apply { + setLayerInset(1, inset, inset, inset, inset) + } + imageView.setImageDrawable(layered) + } } } } @@ -203,6 +220,7 @@ class OCFileListDelegate( } @Suppress("MagicNumber") + @IonosCustomization fun bindViewHolder( viewHolder: ListViewHolder, file: OCFile, @@ -213,7 +231,18 @@ class OCFileListDelegate( // thumbnail viewHolder.imageFileName?.text = file.fileName viewHolder.thumbnail.tag = file.fileId - setThumbnail(viewHolder.thumbnail, viewHolder.shimmerThumbnail, file, overlayManager) + OCFileListThumbnailLoader( + file, + viewHolder, + user, + storageManager, + asyncTasks, + gridView, + context, + preferences, + viewThemeUtils, + syncFolderProvider + ).load() // item layout + click listeners bindGridItemLayout(file, viewHolder) @@ -273,8 +302,9 @@ class OCFileListDelegate( } } + @IonosCustomization private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListViewHolder) { - setItemLayoutBackgroundColor(file, gridViewHolder) + setItemLayoutBackground(file, gridViewHolder) setCheckBoxImage(file, gridViewHolder) setItemLayoutOnClickListeners(file, gridViewHolder) @@ -298,6 +328,26 @@ class OCFileListDelegate( } } + @IonosCustomization + private fun setItemLayoutBackground(file: OCFile, gridViewHolder: ListViewHolder) { + val isSelected = file.fileId == highlightedItem?.fileId || isCheckedFile(file) + if (gridViewHolder is OCFileListGridItemViewHolder) { + val itemLayoutBackgroundResId = if (isSelected) { + R.drawable.grid_mode_selected_item_background + } else { + R.drawable.grid_mode_item_background + } + gridViewHolder.itemLayout.setBackgroundResource(itemLayoutBackgroundResId) + } else { + val itemLayoutBackgroundColor = if (isSelected) { + ContextCompat.getColor(context, R.color.selected_item_background) + } else { + ContextCompat.getColor(context, R.color.bg_default) + } + gridViewHolder.itemLayout.setBackgroundColor(itemLayoutBackgroundColor) + } + } + private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListViewHolder) { val cornerRadius = context.resources.getDimension(R.dimen.selected_grid_container_radius) @@ -320,11 +370,10 @@ class OCFileListDelegate( } } + @IonosCustomization private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListViewHolder) { if (isCheckedFile(file)) { - gridViewHolder.checkbox.setImageDrawable( - viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY) - ) + gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_marked) } else { gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt index 484bf8f62622..ff9b3aed121d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -15,6 +15,8 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.ionos.annotation.IonosCustomization +import com.ionos.player.ui.common.PlayerProgressIndicator import com.owncloud.android.databinding.GridItemBinding class OCFileListGridItemViewHolder(var binding: GridItemBinding) : @@ -34,6 +36,9 @@ class OCFileListGridItemViewHolder(var binding: GridItemBinding) : } override val thumbnail: ImageView get() = binding.thumbnail + @IonosCustomization("Show current playback progress") + override val playerProgressIndicator: PlayerProgressIndicator + get() = binding.playerProgressIndicator override fun showVideoOverlay() { binding.videoOverlay.visibility = View.VISIBLE diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt index 3336b2b62663..22705a0321de 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -16,6 +16,8 @@ import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import com.ionos.annotation.IonosCustomization +import com.ionos.player.ui.common.PlayerProgressIndicator import com.owncloud.android.databinding.ListItemBinding import com.owncloud.android.ui.AvatarGroupLayout @@ -59,6 +61,9 @@ class OCFileListItemViewHolder(private var binding: ListItemBinding) : get() = binding.tagMore override val fileDetailGroup: LinearLayout get() = binding.fileDetailGroup + @IonosCustomization("Show current playback progress") + override val playerProgressIndicator: PlayerProgressIndicator + get() = binding.playerProgressIndicator override fun showVideoOverlay() { binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt index 9d805087a627..2a7e9c2c6e07 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt @@ -15,6 +15,7 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.ionos.player.ui.common.PlayerProgressIndicator import com.owncloud.android.databinding.RecommendedFileItemBinding class OCFileListRecommendedItemViewHolder(private val binding: RecommendedFileItemBinding) : @@ -23,6 +24,8 @@ class OCFileListRecommendedItemViewHolder(private val binding: RecommendedFileIt val reason: TextView get() = binding.reason override val fileName: TextView get() = binding.filename + override val playerProgressIndicator: PlayerProgressIndicator + get() = TODO("Not yet implemented") override val extension: TextView? get() = binding.extension override val thumbnail: ImageView get() = binding.thumbnail override val shimmerThumbnail: LoaderImageView get() = binding.thumbnailShimmer diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListThumbnailLoader.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListThumbnailLoader.kt new file mode 100644 index 000000000000..bb1ea79613ec --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListThumbnailLoader.kt @@ -0,0 +1,157 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui.adapter + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.AsyncTask +import android.view.View +import android.widget.ImageView +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.nextcloud.client.account.User +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderObserver +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncThumbnailDrawable +import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask +import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTaskObject +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils + +class OCFileListThumbnailLoader( + private val file: OCFile, + private val viewHolder: ListViewHolder, + private val user: User, + private val storageManager: FileDataStorageManager, + private val asyncTasks: MutableList, + private val gridView: Boolean, + private val context: Context, + private val preferences: AppPreferences, + private val viewThemeUtils: ViewThemeUtils, + private val syncedFolderProvider: SyncedFolderProvider?, +) { + private val iconView: ImageView? = viewHolder.itemLayout.findViewById(R.id.icon) + private val videoOverlay: ImageView? = viewHolder.itemLayout.findViewById(R.id.videoOverlay) + private val thumbnailView: ImageView = viewHolder.thumbnail + private val shimmerView: LoaderImageView = viewHolder.shimmerThumbnail + + fun load() { + if (file.isFolder) { + showFolderIcon() + } else if (!file.isPreviewAvailable || file.remoteId == null) { + showFileIcon() + } else { + loadFromCacheOrRemote() + } + } + + private fun loadFromCacheOrRemote() { + val cacheKey = ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId + val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey) + if (cachedThumbnail == null || file.isUpdateThumbnailNeeded) { + loadFromRemote() + } else { + showThumbnail(cachedThumbnail) + } + } + + private fun loadFromRemote() { + if (!ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) { + return + } + + showShimmer() + + try { + val task = ThumbnailGenerationTask( + thumbnailView, + storageManager, + user, + asyncTasks, + gridView, + file.remoteId, + ) + + task.setListener(object : ThumbnailGenerationTask.Listener { + override fun onSuccess() { + showExistedThumbnail() + } + + override fun onError() { + showFileIcon() + } + }) + + val px = ThumbnailsCacheManager.getThumbnailDimension() + val tempBitmap = Bitmap.createBitmap(px, px, Bitmap.Config.RGB_565) + val asyncDrawable = AsyncThumbnailDrawable(context.resources, tempBitmap, task) + thumbnailView.setImageDrawable(asyncDrawable) + + asyncTasks.add(task) + val taskObject = ThumbnailGenerationTaskObject(file, file.remoteId) + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, taskObject) + } catch (e: Exception) { + Log_OC.d(this::class.simpleName, "ThumbnailGenerationTask : " + e.message) + } + } + + private fun showFolderIcon() { + val isAutoUploadFolder = SyncedFolderObserver.isAutoUploadFolder(file, user) + val isDarkModeActive = preferences.isDarkModeEnabled + val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder) + val fileIcon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils) + showIcon(fileIcon) + } + + private fun showFileIcon() { + val fileIcon = MimeTypeUtil.getFileTypeIcon(file.mimeType, file.fileName, context, viewThemeUtils) + showIcon(fileIcon) + } + + private fun showIcon(icon: Drawable) { + if (iconView != null) { + iconView.setImageDrawable(icon) + iconView.visibility = View.VISIBLE + thumbnailView.visibility = View.GONE + shimmerView.visibility = View.GONE + videoOverlay?.visibility = View.GONE + } else { + thumbnailView.setImageDrawable(icon) + thumbnailView.visibility = View.VISIBLE + shimmerView.visibility = View.GONE + videoOverlay?.visibility = View.GONE + } + } + + private fun showThumbnail(thumbnail: Bitmap) { + thumbnailView.setImageBitmap(thumbnail) + showExistedThumbnail() + } + + private fun showExistedThumbnail() { + iconView?.visibility = View.GONE + thumbnailView.visibility = View.VISIBLE + shimmerView.visibility = View.GONE + videoOverlay?.visibility = if (MimeTypeUtil.isVideo(file)) View.VISIBLE else View.GONE + } + + private fun showShimmer() { + shimmerView.setImageResource(R.drawable.background) + shimmerView.resetLoader() + iconView?.visibility = View.GONE + thumbnailView.visibility = View.GONE + shimmerView.visibility = View.VISIBLE + videoOverlay?.visibility = View.GONE + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index cd80aa84a0b7..e6aef3ada5fd 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -8,6 +8,7 @@ */ package com.owncloud.android.ui.adapter +import com.ionos.annotation.IonosCustomization import com.nextcloud.utils.extensions.saveShares import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -114,6 +115,7 @@ object OCShareToOCFileConverter { it.modificationTimestamp = -1 } + @IonosCustomization("Set share fileSource as file localId") private fun OCFile.applyShareData(firstShare: OCShare, firstShareTimestamp: Long) = apply { ownerId = firstShare.userId ownerDisplayName = firstShare.ownerDisplayName @@ -122,6 +124,7 @@ object OCShareToOCFileConverter { note = firstShare.note fileId = firstShare.fileSource remoteId = firstShare.remoteId.toString() + localId = firstShare.fileSource this.firstShareTimestamp = firstShareTimestamp isFavorite = firstShare.isFavorite } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt index 4ac0e862160b..7933017fdf58 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt @@ -14,6 +14,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.ionos.annotation.IonosCustomization import com.google.android.material.button.MaterialButton import com.owncloud.android.R import com.owncloud.android.databinding.ItemQuickSharePermissionsBinding @@ -45,6 +46,7 @@ class QuickSharingPermissionsAdapter( private val viewThemeUtils: ViewThemeUtils ) : RecyclerView.ViewHolder(itemView) { + @IonosCustomization("Disable icon tinting") fun bindData(quickPermission: QuickPermission) { val context = itemView.context val permissionName = quickPermission.type.getText(context) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java b/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java index 20706f4d7913..e47c986af534 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java @@ -18,6 +18,8 @@ import android.view.View; import android.widget.ImageView; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.client.account.User; import com.nextcloud.utils.extensions.ImageViewExtensionsKt; import com.owncloud.android.R; @@ -127,22 +129,22 @@ public void bind(OCShare share, } } + @IonosCustomization("Permission name color") private void setPermissionName(String permissionName) { if (!TextUtils.isEmpty(permissionName)) { binding.permissionName.setText(permissionName); binding.permissionName.setVisibility(View.VISIBLE); - viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName); + //viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName); } else { binding.permissionName.setVisibility(View.GONE); } } private void setImage(ImageView avatar, String name) { - if (TextUtils.isEmpty(name)) { + if (IonosBuildHelper.isIonosBuild() || TextUtils.isEmpty(name)) { setUserImage(avatar); return; } - try { avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension)); } catch (StringIndexOutOfBoundsException e) { @@ -150,8 +152,8 @@ private void setImage(ImageView avatar, String name) { } } + @IonosCustomization private void setUserImage(ImageView avatar) { - ImageViewExtensionsKt.makeRoundedWithIcon(avatar, context, R.drawable.ic_user_outline); - viewThemeUtils.platform.colorImageViewBackgroundAndIcon(avatar); + avatar.setImageResource(R.drawable.account_circle_white); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt index 2921f83148da..5afa01d38249 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt @@ -17,6 +17,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView +import com.ionos.annotation.IonosCustomization +import com.ionos.utils.IonosBuildHelper import com.nextcloud.client.account.User import com.nextcloud.utils.mdm.MDMConfig.shareViaLink import com.owncloud.android.R @@ -170,6 +172,7 @@ class ShareeListAdapter( } } + @IonosCustomization("Hide internal share link") fun sortShares() { shares.sortWith( compareBy { @@ -182,7 +185,7 @@ class ShareeListAdapter( ) // add internal share link at end - if (!encrypted && sharesType == SharesType.INTERNAL) { + if (!IonosBuildHelper.isIonosBuild() && !encrypted && sharesType == SharesType.INTERNAL) { shares.add(OCShare().apply { shareType = ShareType.INTERNAL }) } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 126af0068702..2b6223cb3a19 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -20,6 +20,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.LifecycleCoroutineScope import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter import com.afollestad.sectionedrecyclerview.SectionedViewHolder +import com.ionos.annotation.IonosCustomization import com.google.android.material.button.MaterialButton import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.core.Clock @@ -529,10 +530,10 @@ class SyncedFolderAdapter( binding.root ) + @IonosCustomization private fun setSyncButtonActiveIcon(syncStatusButton: MaterialButton, enabled: Boolean) { if (enabled) { - syncStatusButton.icon = - viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_cloud_sync_on, ColorRole.PRIMARY) + syncStatusButton.icon = ContextCompat.getDrawable(context, R.drawable.ic_cloud_sync_on) } else { syncStatusButton.icon = ContextCompat.getDrawable(context, R.drawable.ic_cloud_sync_off) } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java index 3f3e15870adb..6a33fcdf45b8 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java @@ -12,11 +12,13 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.preferences.AppPreferences; @@ -30,6 +32,7 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile; import com.owncloud.android.ui.interfaces.TrashbinActivityInterface; +import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.MimeTypeUtil; @@ -118,6 +121,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int } } + @IonosCustomization("Checkbox style") @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof TrashbinFileViewHolder trashbinFileViewHolder) { @@ -153,8 +157,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi // checkbox if (isCheckedFile(file)) { - trashbinFileViewHolder.binding.customCheckbox.setImageDrawable( - viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY)); + trashbinFileViewHolder.binding.customCheckbox.setImageResource(R.drawable.ic_checkbox_marked); } else { trashbinFileViewHolder.binding.customCheckbox.setImageResource(R.drawable.ic_checkbox_blank_outline); } @@ -250,6 +253,7 @@ private String generateFooterText(int filesCount, int foldersCount) { return output; } + @IonosCustomization("Async thumbnail fix") private void setThumbnail(TrashbinFile file, ImageView thumbnailView) { if (file.isFolder()) { thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context, viewThemeUtils)); @@ -276,6 +280,12 @@ private void setThumbnail(TrashbinFile file, ImageView thumbnailView) { // generate new thumbnail if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) { try { + Drawable drawable = thumbnailView.getDrawable(); + if (drawable != null) { + int px = ThumbnailsCacheManager.getThumbnailDimension(); + thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px); + } + final ThumbnailsCacheManager.ThumbnailGenerationTask task = new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView, storageManager, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt index d4bc6d68114a..c26a33624232 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt @@ -62,6 +62,8 @@ class UnifiedSearchCurrentDirItemViewHolder( overlayManager ) + binding.favoriteAction.visibility = if (file.isFavorite) View.VISIBLE else View.GONE + binding.more.setOnClickListener { action.openFile(file.decryptedRemotePath, true) } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt index 97c32785c018..2b8d4e40b06c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat import androidx.core.widget.ImageViewCompat import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.bumptech.glide.Glide +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.preferences.AppPreferences @@ -63,7 +64,9 @@ class UnifiedSearchItemViewHolder( bindLocalFileIndicator(entry) val entryType = entry.getType() - bindThumbnail(entry, entryType) + val file = storageManager.getFileByRemotePath(entry.remotePath()) + bindThumbnail(file, entry, entryType) + bindFavoriteIndicator(file) bindMoreButton(entry) binding.unifiedSearchItemLayout.setOnClickListener { searchEntryOnClick(entry, entryType) @@ -85,8 +88,11 @@ class UnifiedSearchItemViewHolder( binding.localFileIndicator.setVisibleIf(showLocalFileIndicator) } - private fun bindThumbnail(entry: SearchResultEntry, entryType: SearchResultEntryType) { - val file = storageManager.getFileByRemotePath(entry.remotePath()) + private fun bindFavoriteIndicator(file: OCFile?) { + binding.favoriteAction.setVisibleIf(file?.isFavorite == true) + } + + private fun bindThumbnail(file: OCFile?, entry: SearchResultEntry, entryType: SearchResultEntryType) { Glide.with(context).clear(binding.thumbnail) binding.thumbnailOverlayIcon.setVisibleIf(false) @@ -97,12 +103,10 @@ class UnifiedSearchItemViewHolder( } } + @IonosCustomization("Overlay size fix") private fun bindFolderThumbnail(file: OCFile) { - binding.thumbnail.apply { - setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder)) - viewThemeUtils.platform.colorImageView(this, ColorRole.PRIMARY) - } - overlayManager.setFolderOverlayIcon(file, binding.thumbnailOverlayIcon) + overlayManager.setFolderThumbnail(file, binding.thumbnail, binding.thumbnailShimmer) + binding.thumbnailOverlayIcon.setVisibleIf(false) } private fun bindLocalFileThumbnail(file: OCFile) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java index 6c61b21b688b..834f4748536f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java @@ -21,6 +21,7 @@ import android.view.ViewGroup; import android.widget.ImageView; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.utils.mdm.MDMConfig; @@ -244,7 +245,9 @@ public void bind(User user, * * @param user the account */ + @IonosCustomization("Hide account id") private void setUser(User user) { + binding.account.setVisibility(View.GONE); binding.account.setText(DisplayUtils.convertIdn(user.getAccountName(), false)); binding.account.setTag(user.getAccountName()); } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt index 3516bf4564ce..e6185a6f276a 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt @@ -16,6 +16,8 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization +import com.ionos.player.model.PlaybackModel import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -40,6 +42,9 @@ class AccountRemovalDialog : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var playbackModel: PlaybackModel + private var user: User? = null private lateinit var alertDialog: AlertDialog private var _binding: AccountRemovalDialogBinding? = null @@ -50,6 +55,7 @@ class AccountRemovalDialog : user = requireArguments().getParcelableArgument(KEY_USER, User::class.java) } + @IonosCustomization("Hide account name, radio btn color") override fun onStart() { super.onStart() @@ -57,7 +63,7 @@ class AccountRemovalDialog : alertDialog = dialog as AlertDialog alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false - viewThemeUtils.platform.themeRadioButton(binding.radioLocalRemove) + // viewThemeUtils.platform.themeRadioButton(binding.radioLocalRemove) viewThemeUtils.platform.themeRadioButton(binding.radioRequestDeletion) viewThemeUtils.material.colorMaterialButtonPrimaryTonal( alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton @@ -68,6 +74,7 @@ class AccountRemovalDialog : binding.userName.text = UserAccountManager.getDisplayName(user) binding.account.text = user?.let { DisplayUtils.convertIdn(it.accountName, false) } + binding.account.visibility = View.GONE } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -131,6 +138,7 @@ class AccountRemovalDialog : */ private fun removeAccount() { user?.let { user -> + stopMediaPlayerAndHidePip() if (binding.radioRequestDeletion.isChecked) { DisplayUtils.startLinkIntent(activity, user.server.uri.toString() + DROP_ACCOUNT_URI) } else { @@ -139,6 +147,10 @@ class AccountRemovalDialog : } } + private fun stopMediaPlayerAndHidePip() { + playbackModel.release() + } + /** * Start avatar generation. */ diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt index 226da7768e36..962acebd11f6 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt @@ -8,13 +8,13 @@ */ package com.owncloud.android.ui.dialog +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.fragment.app.DialogFragment -import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.di.Injectable +import com.owncloud.android.R import com.owncloud.android.databinding.LoadingDialogBinding import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject @@ -35,18 +35,13 @@ class LoadingDialog : mMessage = arguments?.getString(ARG_MESSAGE) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = LoadingDialogBinding.inflate(inflater, container, false) + @IonosCustomization + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = LoadingDialogBinding.inflate(layoutInflater) binding.loadingText.text = mMessage - - val loadingDrawable = binding.loadingBar.indeterminateDrawable - if (loadingDrawable != null) { - viewThemeUtils.platform.tintDrawable(requireContext(), loadingDrawable) - } - - viewThemeUtils.platform.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE) - - return binding.root + return MaterialAlertDialogBuilder(requireContext(), R.style.Theme_ownCloud_LoadingDialog) + .setView(binding.root) + .create() } override fun onDestroyView() { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt index b788e2ad6e23..4e16f853af7e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt @@ -14,6 +14,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.di.Injectable import com.owncloud.android.R import com.owncloud.android.databinding.SortingOrderFragmentBinding @@ -51,6 +52,7 @@ class SortingOrderDialogFragment : * * @param binding the parent binding */ + @IonosCustomization("Remove material buttons styling") private fun setupDialogElements(binding: SortingOrderFragmentBinding) { val bindings = listOf( binding.sortByNameAscending to FileSortOrder.SORT_A_TO_Z, @@ -65,11 +67,9 @@ class SortingOrderDialogFragment : view.tag = sortOrder view.let { it.setOnClickListener(OnSortOrderClickListener()) - viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(it) } } - viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.cancel) binding.cancel.setOnClickListener { dismiss() } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt index 32424b7e1e8c..1960e998c9ce 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt @@ -16,6 +16,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.R @@ -61,12 +62,13 @@ class StoragePermissionDialogFragment : } } + @IonosCustomization override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val title = R.string.file_management_permission_optional - val explanationResource = R.string.file_management_permission_optional_text + val title = R.string.ionos_file_management_permission_optional + val explanationResource = R.string.ionos_file_management_permission_optional_text val message = getString(explanationResource, getString(R.string.app_name)) - val dialogBuilder = MaterialAlertDialogBuilder(requireContext()) + val dialogBuilder = MaterialAlertDialogBuilder(requireContext(), R.style.Theme_ownCloud_Dialog) .setTitle(title) .setMessage(message) .setPositiveButton(R.string.storage_permission_all_files_access) { _, _ -> diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt index eb4ae2adfffd..174647d019cb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt @@ -50,6 +50,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.google.android.material.button.MaterialButton +import com.ionos.annotation.IonosCustomization +import com.ionos.utils.IonosBuildHelper import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -353,9 +355,14 @@ open class ExtendedListFragment : binding = null } + @IonosCustomization private inner class ScaleListener : SimpleOnScaleGestureListener() { @SuppressLint("NotifyDataSetChanged") override fun onScale(detector: ScaleGestureDetector): Boolean { + if (IonosBuildHelper.isIonosBuild()) { + return true; + } + setGridViewColumns(detector.getScaleFactor()) preferences.setGridColumns(mScale) @@ -549,6 +556,17 @@ open class ExtendedListFragment : } } + /** + * displays an empty list information with a headline, a message and a not to be tinted icon. + * + * @param headline the headline + * @param message the message + * @param icon the icon to be shown + */ + fun setMessageForEmptyList(@StringRes headline: Int, @StringRes message: Int, @DrawableRes icon: Int) { + setMessageForEmptyList(headline, message, icon, false) + } + /** * displays an empty list information with a headline, a message and an icon. * @@ -599,6 +617,7 @@ open class ExtendedListFragment : } @Suppress("LongMethod") + @IonosCustomization("disabled icon tinting") fun setEmptyListMessage(state: Parcelable?) { when (state) { SearchType.NO_SEARCH -> { @@ -606,7 +625,7 @@ open class ExtendedListFragment : R.string.file_list_empty_headline, getEmptyListViewTextId(), R.drawable.ic_list_empty_folder, - true + false ) } @@ -614,7 +633,7 @@ open class ExtendedListFragment : setMessageForEmptyList( R.string.file_list_empty_headline_server_search, R.string.file_list_empty, - R.drawable.ic_search_light_grey + R.drawable.ic_search ) } @@ -622,7 +641,7 @@ open class ExtendedListFragment : setMessageForEmptyList( R.string.file_list_empty_favorite_headline, R.string.file_list_empty_favorites_filter_list, - R.drawable.ic_star_light_yellow + R.drawable.favorite ) } @@ -638,7 +657,7 @@ open class ExtendedListFragment : setMessageForEmptyList( R.string.file_list_empty_headline_search, R.string.file_list_empty_search, - R.drawable.ic_search_light_grey + R.drawable.ic_search ) } @@ -662,7 +681,7 @@ open class ExtendedListFragment : setMessageForEmptyList( R.string.file_list_empty_headline_server_search, R.string.file_list_empty_local_search, - R.drawable.ic_search_light_grey + R.drawable.ic_search ) } @@ -671,7 +690,7 @@ open class ExtendedListFragment : R.string.offline_mode_info_title, R.string.offline_mode_info_description, R.drawable.ic_cloud_sync, - true + false ) } @@ -688,7 +707,7 @@ open class ExtendedListFragment : R.string.folder_list_empty_headline, R.string.file_list_empty_moving, R.drawable.ic_list_empty_create_folder, - true + false ) } @@ -697,7 +716,7 @@ open class ExtendedListFragment : R.string.file_list_empty_headline, R.string.file_list_empty_on_device, R.drawable.ic_list_empty_folder, - true + false ) } @@ -705,8 +724,8 @@ open class ExtendedListFragment : setMessageForEmptyList( R.string.file_list_empty_headline, R.string.local_file_list_empty, - R.drawable.ic_list_empty_folder, - true + R.drawable.ic_list_empty_create_folder, + false ) } @@ -715,7 +734,7 @@ open class ExtendedListFragment : R.string.folder_list_empty_headline, R.string.local_folder_list_empty, R.drawable.ic_list_empty_folder, - true + false ) } @@ -733,7 +752,7 @@ open class ExtendedListFragment : R.string.file_list_empty_headline, getEmptyListViewTextId(), R.drawable.ic_list_empty_folder, - true + false ) } }.also { @@ -768,6 +787,11 @@ open class ExtendedListFragment : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) + if (IonosBuildHelper.isIonosBuild()) { + mScale = preferences.getGridColumns(); + setGridViewColumns(1f); + return; + } if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { maxColumnSize = 10 diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 4774dfc5f996..0b5365e5f29c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -23,6 +23,8 @@ import com.google.android.material.chip.Chip; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; @@ -301,7 +303,15 @@ private void onOverflowIconClicked() { .show(fragmentManager, "actions"); } + @IonosCustomization("Hide tabs in IONOS") private void setupViewPager() { + if (IonosBuildHelper.isIonosBuild()) { + FileDetailTabAdapter adapter = new FileDetailTabAdapter(requireActivity(), getFile(), user, showSharingTab(), showDetailsTab()); + binding.pager.setAdapter(adapter); + binding.tabLayout.setVisibility(View.GONE); + return; + } + binding.tabLayout.removeAllTabs(); binding.tabLayout.addTab( @@ -375,11 +385,9 @@ public void onTabReselected(TabLayout.Tab tab) { }); binding.tabLayout.post(() -> { - if (binding != null) { - TabLayout.Tab tab = binding.tabLayout.getTabAt(activeTab); - if (tab == null) return; - tab.select(); - } + TabLayout.Tab tab1 = binding.tabLayout.getTabAt(activeTab); + if (tab1 == null) return; + tab1.select(); }); } @@ -841,8 +849,9 @@ public void initiateSharingProcess(String shareeName, * * @param isFragmentReplaced */ + @IonosCustomization("Hide tabs in IONOS") public void showHideFragmentView(boolean isFragmentReplaced) { - binding.tabLayout.setVisibility(isFragmentReplaced ? View.GONE : View.VISIBLE); + binding.tabLayout.setVisibility(View.GONE); binding.pager.setVisibility(isFragmentReplaced ? View.GONE : View.VISIBLE); binding.sharingFrameContainer.setVisibility(isFragmentReplaced ? View.VISIBLE : View.GONE); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index cdacde73a97f..25a71d05f945 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -310,6 +311,7 @@ class FileDetailSharingFragment : } } + @IonosCustomization("Search view theming") private fun themeView(binding: FileDetailsSharingFragmentBinding) { binding.run { viewThemeUtils.material.run { @@ -329,6 +331,8 @@ class FileDetailSharingFragment : viewThemeUtils.files.run { themeContentSearchView(searchView) } + + viewThemeUtils.androidx.themeToolbarSearchView(searchView) } } @@ -392,13 +396,14 @@ class FileDetailSharingFragment : toggleSearchViewEnable(searchView, false) } + @IonosCustomization("Share search hint") private fun setupShareView(binding: FileDetailsSharingFragmentBinding, parentFile: OCFile?) { binding.run { if (file?.isEncrypted == true || (parentFile != null && parentFile.isEncrypted)) { setupViewForEncryptedShare(this) } else { createLink.setText(R.string.create_link) - searchView.setQueryHint(getResources().getString(R.string.share_search_internal)) + searchView.setQueryHint(getResources().getString(R.string.ionos_share_search)) } createLink.setOnClickListener { createPublicShareLink() } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java index 7954884a1340..282a03c8380c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java @@ -12,12 +12,10 @@ import android.os.Bundle; import android.view.View; -import android.view.ViewGroup; -import androidx.core.content.ContextCompat; -import com.owncloud.android.R; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.databinding.FileDetailsSharingMenuBottomSheetFragmentBinding; @@ -50,27 +48,14 @@ public FileDetailSharingMenuBottomSheetDialog(FileActivity fileActivity, } @Override + @IonosCustomization("Remove custom window LayoutParams. Disable icon tinting") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = FileDetailsSharingMenuBottomSheetFragmentBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - if (getWindow() != null) { - getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } - viewThemeUtils.platform.themeDialog(binding.getRoot()); - int textColor = ContextCompat.getColor(getContext(), R.color.text_color); - viewThemeUtils.material.colorMaterialButtonContent(binding.menuShareAdvancedPermissions, ColorRole.PRIMARY); - viewThemeUtils.material.colorMaterialButtonContent(binding.menuShareSendNewEmail, ColorRole.PRIMARY); - viewThemeUtils.material.colorMaterialButtonContent(binding.menuShareSendLink, ColorRole.PRIMARY); - viewThemeUtils.material.colorMaterialButtonContent(binding.menuShareUnshare, ColorRole.PRIMARY); - binding.menuShareAdvancedPermissions.setTextColor(textColor); - binding.menuShareSendNewEmail.setTextColor(textColor); - binding.menuShareSendLink.setTextColor(textColor); - binding.menuShareUnshare.setTextColor(textColor); - updateUI(); setupClickListener(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt index 1f4375466bc7..68f502a9f665 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.adapter.OCFileListAdapter @@ -21,8 +22,9 @@ import com.owncloud.android.utils.FileSortOrder class FileListLayoutManager(private val fragment: OCFileListFragment, private val preferences: AppPreferences) { + @IonosCustomization("Sort button icon") fun sortFiles(sortOrder: FileSortOrder?) { - fragment.mSortButton?.setText(DisplayUtils.getSortOrderStringId(sortOrder)) + fragment.mSortButton?.setIconResource(DisplayUtils.getSortOrderIconRes(sortOrder)) sortOrder?.let { fragment.mAdapter.setSortOrder(fragment.mFile, it) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt index a2be5cf3bb99..c9c7da0eca3e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.ionos.annotation.IonosCustomization import com.nextcloud.utils.extensions.getAllGalleryItemsSuspended import com.nextcloud.utils.extensions.getParcelableArgument import kotlinx.coroutines.Job @@ -452,7 +453,9 @@ class GalleryFragment : const val REFRESH_SEARCH_EVENT_RECEIVER: String = "refreshSearchEventReceiver" private const val MAX_LANDSCAPE_COLUMN_SIZE = 5 - private const val MAX_PORTRAIT_COLUMN_SIZE = 2 + + @IonosCustomization("increased quantity") + private const val MAX_PORTRAIT_COLUMN_SIZE = 3 fun setLastMediaItemPosition(position: Int?) { lastMediaItemPosition = position diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt index 5d5a4b2576c9..9d4b1db6894c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt @@ -16,6 +16,7 @@ import android.view.ViewGroup import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.di.Injectable import com.owncloud.android.R @@ -51,24 +52,10 @@ class GalleryFragmentBottomSheetDialog : setupClickListener() } + @IonosCustomization private fun setupLayout() { viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) - listOf( - binding.tickMarkShowImages, - binding.tickMarkShowVideos - ).forEach { - viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) - } - - listOf( - binding.btnSelectMediaFolder, - binding.btnHideVideos, - binding.btnHideImages - ).forEach { - viewThemeUtils.material.colorMaterialButtonText(it) - } - when (currentMediaState) { MediaState.MEDIA_STATE_PHOTOS_ONLY -> { binding.tickMarkShowImages.visibility = View.VISIBLE diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java index 14d5cbc9cf6e..432891d6136c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java @@ -20,6 +20,7 @@ import android.view.View; import android.view.ViewGroup; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; import com.owncloud.android.lib.common.utils.Log_OC; @@ -86,14 +87,17 @@ public void onAttach(@NonNull Activity activity) { * {@inheritDoc} */ @Override + @IonosCustomization public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log_OC.i(TAG, "onCreateView() start"); View v = super.onCreateView(inflater, container, savedInstanceState); - if (mContainerActivity.isFolderPickerMode()) { - setEmptyListMessage(EmptyListState.LOCAL_FILE_LIST_EMPTY_FOLDER); + if (!mContainerActivity.isFolderPickerMode()) { + setMessageForEmptyList(R.string.file_list_empty_headline, R.string.local_file_list_empty, + R.drawable.ic_list_empty_folder); } else { - setEmptyListMessage(EmptyListState.LOCAL_FILE_LIST_EMPTY_FILE); + setMessageForEmptyList(R.string.folder_list_empty_headline, R.string.local_folder_list_empty, + R.drawable.ic_list_empty_folder); } setSwipeEnabled(false); // Disable pull-to-refresh @@ -107,6 +111,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, * {@inheritDoc} */ @Override + @IonosCustomization("mSortButton icon") public void onActivityCreated(Bundle savedInstanceState) { Log_OC.i(TAG, "onActivityCreated() start"); @@ -130,7 +135,7 @@ public void onActivityCreated(Bundle savedInstanceState) { FileSortOrder sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.localFileListView); if (sortOrder != null) { - mSortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)); + mSortButton.setIconResource(DisplayUtils.getSortOrderIconRes(sortOrder)); } } @@ -300,10 +305,9 @@ public int getFilesCount() { return mAdapter.getFilesCount(); } + @IonosCustomization public void sortFiles(FileSortOrder sortOrder) { - if (mSortButton != null) { - mSortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)); - } + mSortButton.setIconResource(DisplayUtils.getSortOrderIconRes(sortOrder)); mAdapter.setSortOrder(sortOrder); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 87675ea6ff65..87e2ba54d2b5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -39,13 +39,15 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; +import com.ionos.annotation.IonosCustomization; +import com.ionos.scanbot.controller.ScanbotController; +import com.ionos.scanbot.ui.ScanbotCameraPermissionHandler; import com.nextcloud.android.lib.resources.clientintegration.Endpoint; import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation; import com.nextcloud.client.account.User; import com.nextcloud.client.device.DeviceInfo; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.documentscan.AppScanOptionalFeature; -import com.nextcloud.client.documentscan.DocumentScanActivity; import com.nextcloud.client.editimage.EditImageActivity; import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.network.ClientFactory; @@ -118,7 +120,6 @@ import com.owncloud.android.utils.EncryptionUtilsV2; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.PermissionUtil; import com.owncloud.android.utils.overlay.OverlayManager; import com.owncloud.android.utils.theme.ThemeUtils; @@ -208,6 +209,7 @@ public class OCFileListFragment extends ExtendedListFragment implements @Inject ShortcutUtil shortcutUtil; @Inject SyncedFolderProvider syncedFolderProvider; @Inject AppScanOptionalFeature appScanOptionalFeature; + @Inject ScanbotController scanbotController; @Inject OverlayManager overlayManager; protected FileFragment.ContainerActivity mContainerActivity; @@ -262,10 +264,16 @@ public void onCreate(Bundle savedInstanceState) { searchFragment = currentSearchType != null && isSearchEventSet(searchEvent); } + @IonosCustomization("Subscribe to Scanbot events") @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); listenSetupEncryptionDialogResult(); + getViewLifecycleOwner().getLifecycle().addObserver( + new ScanbotCameraPermissionHandler( + scanbotController, + () -> isAdded() && !isDetached() ? getView() : null + )); } @Override @@ -291,6 +299,10 @@ public void onResume() { handleSearchEvent(searchEvent); } + if (getActivity() instanceof FileDisplayActivity fda) { + fda.startSyncFolderOperation(getCurrentFile(), true); + } + super.onResume(); } @@ -348,6 +360,7 @@ public void setSearchArgs(Bundle state) { * {@inheritDoc} */ @Override + @IonosCustomization public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log_OC.i(TAG, "onCreateView() start"); View v = super.onCreateView(inflater, container, savedInstanceState); @@ -362,11 +375,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, mFabMain = requireActivity().findViewById(R.id.fab_main); - if (mFabMain != null) { - // is not available in FolderPickerActivity - viewThemeUtils.material.themeFAB(mFabMain); - } - Log_OC.i(TAG, "onCreateView() end"); return v; } @@ -502,6 +510,7 @@ protected void prepareCurrentSearch(SearchEvent event) { /** * register listener on FAB. */ + @IonosCustomization("Show simple FAB menu bottom sheet") public void registerFabListener() { if (!(getActivity() instanceof FileActivity fileActivity)) { Log_OC.w(TAG, "activity is null cannot register fab listener"); @@ -514,7 +523,6 @@ public void registerFabListener() { } // is not available in FolderPickerActivity - viewThemeUtils.material.themeFAB(mFabMain); mFabMain.setOnClickListener(v -> { var currentDir = getCurrentFile(); if (currentDir == null) { @@ -522,14 +530,11 @@ public void registerFabListener() { return; } - final OCFileListBottomSheetDialog dialog = new OCFileListBottomSheetDialog(fileActivity, + final SimpleOCFileListBottomSheetDialog dialog = new SimpleOCFileListBottomSheetDialog(fileActivity, this, deviceInfo, - accountManager.getUser(), - currentDir, themeUtils, viewThemeUtils, - editorUtils, appScanOptionalFeature); dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); @@ -615,9 +620,7 @@ public void scanDocUpload() { final OCFile currentFile = getCurrentFile(); if (fileDisplayActivity != null && currentFile != null && currentFile.isFolder()) { - Intent intent = new Intent(requireContext(), DocumentScanActivity.class); - intent.putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.getRemotePath()); - startActivity(intent); + scanbotController.scanToDocument(requireContext(), currentFile.getRemotePath()); } else { Log.w(TAG, "scanDocUpload: Failed to start doc scanning, fileDisplayActivity=" + fileDisplayActivity + ", currentFile=" + currentFile); @@ -856,6 +859,7 @@ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, bo * Load menu and customize UI when action mode is started. */ @Override + @IonosCustomization public boolean onCreateActionMode(ActionMode mode, Menu menu) { mActiveActionMode = mode; // Determine if actionMode is "new" or not (already affected by item-selection) @@ -865,10 +869,10 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = requireActivity().getMenuInflater(); inflater.inflate(R.menu.custom_menu_placeholder, menu); - final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item); - if (item.getIcon() != null) { - item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white))); - } +// final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item); +// if (item.getIcon() != null) { +// item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white))); +// } mActiveActionMode.invalidate(); @@ -1205,6 +1209,7 @@ private void fileOnItemClick(OCFile file) { } } + @IonosCustomization("Video in custom player") private void handlePendingDownloadFile(OCFile file) { if (!isAccountManagerInitialized()) { Log_OC.e(TAG, "AccountManager not yet initialized"); @@ -1214,10 +1219,7 @@ private void handlePendingDownloadFile(OCFile file) { User account = accountManager.getUser(); OCCapability capability = mContainerActivity.getStorageManager().getCapability(account.getAccountName()); - if (MimeTypeUtil.isVideo(file) && !file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda) { - setFabVisible(false); - fda.startImagePreview(file, true, null); - } else if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda) { + if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda) { setFabVisible(false); fda.startMediaPreview(file, 0, true, true, true, true); } else if (editorUtils.isEditorAvailable(accountManager.getUser(), file.getMimeType()) && !file.isEncrypted()) { @@ -1645,7 +1647,8 @@ private void updateLayout() { invalidateActionMode(); } - + + @IonosCustomization("Sort button icon") private void updateSortButton() { if (mSortButton != null) { FileSortOrder sortOrder; @@ -1655,7 +1658,7 @@ private void updateSortButton() { sortOrder = preferences.getSortOrderByFolder(mFile); } - mSortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)); + mSortButton.setIconResource(DisplayUtils.getSortOrderIconRes(preferences.getSortOrderByFolder(mFile))); } } @@ -2268,6 +2271,7 @@ public boolean isLoading() { * * @param visible Desired visibility for the FAB. */ + @IonosCustomization("Removed FAB theming") public void setFabVisible(final boolean visible) { if (mFabMain == null) { // is not available in FolderPickerActivity @@ -2282,7 +2286,6 @@ public void setFabVisible(final boolean visible) { activity.runOnUiThread(() -> { if (visible) { mFabMain.show(); - viewThemeUtils.material.themeFAB(mFabMain); } else { mFabMain.hide(); } @@ -2304,6 +2307,7 @@ public void slideHideBottomBehaviourForBottomNavigationView(boolean visible) { * * @param enabled Desired visibility for the FAB. */ + @IonosCustomization public void setFabEnabled(final boolean enabled) { if (mFabMain == null) { // is not available in FolderPickerActivity @@ -2314,10 +2318,8 @@ public void setFabEnabled(final boolean enabled) { getActivity().runOnUiThread(() -> { if (enabled) { mFabMain.setEnabled(true); - viewThemeUtils.material.themeFAB(mFabMain); } else { mFabMain.setEnabled(false); - viewThemeUtils.material.themeFAB(mFabMain); } }); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java index 75ae214383f3..69295eb06c3f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java @@ -13,10 +13,10 @@ import android.app.Dialog; import android.os.Bundle; import android.view.View; -import android.view.ViewGroup; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.utils.extensions.OCShareExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.databinding.QuickSharingPermissionsBottomSheetFragmentBinding; @@ -62,15 +62,12 @@ public QuickSharingPermissionsBottomSheetDialog(FileActivity fileActivity, } @Override + @IonosCustomization("Remove custom window LayoutParams") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = QuickSharingPermissionsBottomSheetFragmentBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - if (getWindow() != null) { - getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } - viewThemeUtils.platform.themeDialog(binding.getRoot()); setUpRecyclerView(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SimpleOCFileListBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/SimpleOCFileListBottomSheetDialog.java new file mode 100644 index 000000000000..abacc83116f7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SimpleOCFileListBottomSheetDialog.java @@ -0,0 +1,121 @@ +/* + * IONOS HiDrive Next - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: GPL-2.0 + */ + +package com.owncloud.android.ui.fragment; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; +import com.nextcloud.client.device.DeviceInfo; +import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.documentscan.AppScanOptionalFeature; +import com.owncloud.android.MainApp; +import com.nextcloud.utils.BuildHelper; +import com.owncloud.android.R; +import com.owncloud.android.databinding.SimpleFileListActionsBottomSheetFragmentBinding; +import com.owncloud.android.ui.activity.FileActivity; +import com.owncloud.android.utils.PermissionUtil; +import com.owncloud.android.utils.theme.ThemeUtils; +import com.owncloud.android.utils.theme.ViewThemeUtils; + +/** + * Simple FAB menu {@link android.app.Dialog} styled as a bottom sheet for main actions. + */ +public class SimpleOCFileListBottomSheetDialog extends BottomSheetDialog implements Injectable { + + private SimpleFileListActionsBottomSheetFragmentBinding binding; + private final OCFileListBottomSheetActions actions; + private final DeviceInfo deviceInfo; + private final ThemeUtils themeUtils; + private final ViewThemeUtils viewThemeUtils; + + private final AppScanOptionalFeature appScanOptionalFeature; + + public SimpleOCFileListBottomSheetDialog(FileActivity fileActivity, + OCFileListBottomSheetActions actions, + DeviceInfo deviceInfo, + ThemeUtils themeUtils, + ViewThemeUtils viewThemeUtils, + AppScanOptionalFeature appScanOptionalFeature) { + super(fileActivity); + this.actions = actions; + this.deviceInfo = deviceInfo; + this.themeUtils = themeUtils; + this.viewThemeUtils = viewThemeUtils; + this.appScanOptionalFeature = appScanOptionalFeature; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = SimpleFileListActionsBottomSheetFragmentBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + viewThemeUtils.platform.colorViewBackground(binding.getRoot(), ColorRole.SURFACE); + + binding.addToCloud.setText(getContext().getResources().getString(R.string.add_to_cloud, + themeUtils.getDefaultDisplayNameForRootFolder(getContext()))); + + if (!deviceInfo.hasCamera(getContext())) { + binding.menuDirectCameraUpload.setVisibility(View.GONE); + } + + setupClickListener(); + + if (MainApp.isClientBranded() && BuildHelper.INSTANCE.isFlavourGPlay()) { + // this way we can have branded clients with that permission + boolean hasPermission = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + hasPermission = PermissionUtil.INSTANCE.manifestHasAllFilesPermission(getContext()); + } + if (!hasPermission) { + binding.menuUploadFiles.setVisibility(View.GONE); +// binding.uploadContentFromOtherApps.setText(getContext().getString(R.string.upload_files)); + } + } + } + + private void setupClickListener() { + binding.menuMkdir.setOnClickListener(v -> { + actions.createFolder(false); + dismiss(); + }); + + binding.menuUploadFromApp.setOnClickListener(v -> { + actions.uploadFromApp(); + dismiss(); + }); + + binding.menuDirectCameraUpload.setOnClickListener(v -> { + actions.directCameraUpload(); + dismiss(); + }); + + if (appScanOptionalFeature.isAvailable()) { + binding.menuScanDocUpload.setOnClickListener(v -> { + actions.scanDocUpload(); + dismiss(); + }); + } else { + binding.menuScanDocUpload.setVisibility(View.GONE); + } + + binding.menuUploadFiles.setOnClickListener(v -> { + actions.uploadFiles(); + dismiss(); + }); + } + + @Override + protected void onStop() { + super.onStop(); + binding = null; + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt index 31abab9602fc..3cd74c4d7d49 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt @@ -28,6 +28,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.AsyncRunner @@ -156,11 +157,13 @@ class UnifiedSearchFragment : ?: ArrayList() } + @IonosCustomization("themeSwipeRefreshLayout") @Suppress("DEPRECATION") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = ListFragmentBinding.inflate(inflater, container, false) binding.listRoot.updatePadding(top = resources.getDimension(R.dimen.standard_half_padding).toInt()) setUpBinding() + viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) setHasOptionsMenu(true) return binding.root @@ -171,7 +174,7 @@ class UnifiedSearchFragment : setupAdapter() if (supportsOpeningCalendarContactsLocally()) { - checkPermissions() + // checkPermissions() } } @@ -182,6 +185,10 @@ class UnifiedSearchFragment : setMainFabVisible(false) updateActionBarTitleAndHomeButtonByString(null) setDrawerIndicatorEnabled(false) + + supportActionBar?.let { actionBar -> + viewThemeUtils.files.themeActionBar(this, actionBar) + } } } @@ -265,13 +272,12 @@ class UnifiedSearchFragment : } } + @IonosCustomization("setImageResource") private fun showEmptyView(state: UnifiedSearchFragmentScreenState.Empty) { toggleEmptyListVisible(show = true) binding.emptyList.run { - emptyListIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable(requireContext(), state.iconId) - ) + emptyListIcon.setImageResource(state.iconId) emptyListViewHeadline.text = requireContext().getString(state.titleId) emptyListViewText.text = requireContext().getString(state.descriptionId) } @@ -315,6 +321,7 @@ class UnifiedSearchFragment : } @Suppress("ComplexCondition") + @IonosCustomization private fun setUpViewModel() { vm.searchResults.observe(viewLifecycleOwner, this::onSearchResultChanged) vm.isLoading.observe(viewLifecycleOwner) { loading -> diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentScreenState.kt b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentScreenState.kt index 8a586b5f6583..31ecae67abba 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentScreenState.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentScreenState.kt @@ -25,13 +25,13 @@ sealed class UnifiedSearchFragmentScreenState { fun startSearch() = Empty( titleId = R.string.file_list_empty_unified_search_start_search, descriptionId = R.string.file_list_empty_unified_search_start_search_description, - iconId = R.drawable.ic_search_grey + iconId = R.drawable.ic_search ) fun noResults() = Empty( titleId = R.string.file_list_empty_headline_server_search, descriptionId = R.string.file_list_empty_unified_search_no_results, - iconId = R.drawable.ic_search_grey + iconId = R.drawable.ic_search ) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt index 43263ad6e441..4ef418089325 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt @@ -27,6 +27,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.BackgroundJobManager @@ -235,22 +236,8 @@ class BackupFragment : } } + @IonosCustomization("Removed theming") private fun applyUserColor() { - viewThemeUtils.androidx.run { - colorSwitchCompat(binding.contacts) - colorSwitchCompat(binding.calendar) - colorSwitchCompat(binding.dailyBackup) - } - - viewThemeUtils.material.run { - colorMaterialButtonPrimaryFilled(binding.backupNow) - colorMaterialButtonPrimaryOutlined(binding.contactsDatepicker) - } - - viewThemeUtils.platform.run { - colorTextView(binding.dataToBackUpTitle) - colorTextView(binding.backupSettingsTitle) - } } private fun addMenuProvider() { @@ -374,6 +361,7 @@ class BackupFragment : if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) openDate(null) } + @IonosCustomization("Removed date picker buttons theming") private fun openDate(savedDate: Calendar?) { val contactsPreferenceActivity = activity as? ContactsPreferenceActivity ?: run { activity?.let { DisplayUtils.showSnackMessage(it, R.string.error_choosing_date) } @@ -400,11 +388,6 @@ class BackupFragment : setOnDismissListener { selectedDate = null } setTitle("") show() - - viewThemeUtils.platform.colorTextButtons( - getButton(DatePickerDialog.BUTTON_NEGATIVE), - getButton(DatePickerDialog.BUTTON_POSITIVE) - ) } } diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/CompletionCallback.kt b/app/src/main/java/com/owncloud/android/ui/interfaces/CompletionCallback.kt new file mode 100644 index 000000000000..d0ffd3e2e70a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/CompletionCallback.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.interfaces + +interface CompletionCallback { + fun onComplete(value: Boolean) +} diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index 703dcc4b9f3c..fab458e08137 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -25,6 +25,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.SyncedFolderEntity import com.nextcloud.client.di.Injectable @@ -58,6 +59,10 @@ import com.owncloud.android.ui.fragment.GalleryFragment import com.owncloud.android.ui.preview.model.PreviewImageActivityState import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil +import android.graphics.drawable.ColorDrawable +import android.view.ViewGroup +import androidx.activity.enableEdgeToEdge +import androidx.core.view.updatePadding import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import java.io.Serializable import javax.inject.Inject @@ -98,9 +103,20 @@ class PreviewImageActivity : @Inject lateinit var localBroadcastManager: LocalBroadcastManager + @IonosCustomization override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) + val contentContainer = (window.decorView as ViewGroup).getChildAt(0) + ViewCompat.setOnApplyWindowInsetsListener(contentContainer) { view, windowInsets -> + val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val insets = windowInsets.getInsets(insetsType) + val actionBarView = view.findViewById(androidx.appcompat.R.id.action_bar) + actionBarView?.updatePadding(left = insets.left, top = insets.top, right = insets.right) + WindowInsetsCompat.CONSUMED + } + if (savedInstanceState != null && !savedInstanceState.getBoolean( KEY_SYSTEM_VISIBLE, @@ -119,11 +135,14 @@ class PreviewImageActivity : val chosenFile = intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + updateActionBarTitleAndHomeButton(chosenFile) + + viewThemeUtils.platform.themeStatusBar(this, getColor(R.color.preview_image_system_bars_color)) supportActionBar?.let { updateActionBarTitleAndHomeButton(chosenFile) viewThemeUtils.files.setWhiteBackButton(this, it) it.setDisplayHomeAsUpEnabled(true) - it.setBackgroundDrawable(R.color.black.toDrawable()) + it.setBackgroundDrawable(R.color.preview_image_system_bars_color.toDrawable()) } setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) @@ -145,6 +164,9 @@ class PreviewImageActivity : sendShareDownloader.downloadFile(file, packageName, activityName) } + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + override fun getMenuItemId(): Int = R.id.nav_gallery private fun applyDisplayCutOutTopPadding() { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index 9326613dd511..aaea6d604d06 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -43,6 +43,7 @@ import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVGParseException import com.github.chrisbanes.photoview.PhotoView import com.google.android.material.snackbar.Snackbar +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.BackgroundJobManager @@ -262,6 +263,7 @@ class PreviewImageFragment : } } + @IonosCustomization private fun adjustResizedImage(thumbnail: Bitmap?, width: Int, height: Int) { var resizedImage = getResizedBitmap(file, width, height) @@ -270,7 +272,7 @@ class PreviewImageFragment : binding.image.visibility = View.VISIBLE binding.emptyListView.visibility = View.GONE binding.emptyListProgress.visibility = View.GONE - binding.image.setBackgroundColor(resources.getColor(R.color.background_color_inverse)) + binding.image.setBackgroundColor(resources.getColor(R.color.preview_image_background)) bitmap = resizedImage } else { @@ -286,7 +288,7 @@ class PreviewImageFragment : containerActivity.storageManager, connectivityService, containerActivity.storageManager.user, - resources.getColor(R.color.background_color_inverse) + resources.getColor(R.color.preview_image_background) ) if (resizedImage == null) { resizedImage = thumbnail @@ -578,6 +580,7 @@ class PreviewImageFragment : } } + @IonosCustomization private fun showLoadedImage(result: LoadImage?) { val imageView = imageViewRef.get() val bitmap = result?.bitmap @@ -619,7 +622,7 @@ class PreviewImageFragment : val progressView = progressViewRef.get() progressView?.visibility = View.GONE - imageView.setBackgroundColor(resources.getColor(R.color.background_color_inverse)) + imageView.setBackgroundColor(resources.getColor(R.color.preview_image_background)) imageView.visibility = View.VISIBLE } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt index 55b653f5e46d..c2ceb56c3cc7 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt @@ -18,9 +18,11 @@ import android.content.Intent import android.content.res.Configuration import android.graphics.BitmapFactory import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.os.AsyncTask +import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -57,6 +59,7 @@ import androidx.media3.ui.PlayerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.download.FileDownloadHelper @@ -177,6 +180,9 @@ class PreviewMediaActivity : } } + @IonosCustomization("Remove window insets paddings") + override fun isDefaultWindowInsetsHandlingEnabled() = false + private fun sendAudioSessionReleaseBroadcast() { val intent = Intent(BackgroundPlayerService.RELEASE_MEDIA_SESSION_BROADCAST_ACTION).apply { setPackage(packageName) @@ -226,6 +232,7 @@ class PreviewMediaActivity : private fun isFileVideo(): Boolean = MimeTypeUtil.isVideo(file) + @IonosCustomization("System bar colors") private fun configureSystemBars() { updateActionBarTitleAndHomeButton(file) @@ -247,12 +254,16 @@ class PreviewMediaActivity : ) it.setBackgroundDrawable(Color.BLACK.toDrawable()) + + viewThemeUtils.platform.themeStatusBar(this, getColor(R.color.transparent)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + + } else { + viewThemeUtils.platform.themeStatusBar(this) } } - - viewThemeUtils.platform.themeStatusBar( - this - ) } private fun showProgressLayout() { @@ -261,6 +272,7 @@ class PreviewMediaActivity : binding.emptyView.emptyListView.visibility = View.GONE } + @IonosCustomization("ui bugfix") private fun setErrorMessage(headline: String, @StringRes message: Int) { binding.emptyView.run { emptyListViewHeadline.text = headline @@ -278,15 +290,9 @@ class PreviewMediaActivity : binding.imagePreview.setImageDrawable(genericThumbnail()) } + @IonosCustomization("No generic thumbnail") private fun genericThumbnail(): Drawable? { - val result = AppCompatResources.getDrawable(this, R.drawable.logo) - result?.let { - if (!resources.getBoolean(R.bool.is_branded_client)) { - DrawableCompat.setTint(it, resources.getColor(R.color.primary, this.theme)) - } - } - - return result + return null } override fun onSaveInstanceState(outState: Bundle) { @@ -455,10 +461,12 @@ class PreviewMediaActivity : } } + @IonosCustomization("Insets for media controller") private fun applyWindowInsets() { val playerView = exoplayerView val exoControls = playerView.findViewById(androidx.media3.ui.R.id.exo_bottom_bar) val exoProgress = playerView.findViewById(androidx.media3.ui.R.id.exo_progress) + val exoControlsHeight = exoControls.layoutParams.height val progressBottomMargin = exoProgress.marginBottom ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> @@ -467,22 +475,21 @@ class PreviewMediaActivity : .displayCutout() ) - binding.materialToolbar.updateLayoutParams { - topMargin = insets.top - } exoControls.updateLayoutParams { - bottomMargin = insets.bottom + height = insets.bottom + exoControlsHeight } exoProgress.updateLayoutParams { bottomMargin = insets.bottom + progressBottomMargin } exoControls.updatePadding(left = insets.left, right = insets.right) + binding.audioControllerView.updatePadding(left = insets.left, right = insets.right, bottom = insets.bottom) exoProgress.updatePadding(left = insets.left, right = insets.right) - binding.materialToolbar.updatePadding(left = insets.left, right = insets.right) + binding.materialToolbar.updatePadding(left = insets.left, top = insets.top, right = insets.right) WindowInsetsCompat.CONSUMED } } + @IonosCustomization("Hide cantrolls animation fix") private fun setupVideoView() { initWindowInsetsController() val type = WindowInsetsCompat.Type.systemBars() @@ -498,6 +505,12 @@ class PreviewMediaActivity : windowInsetsController.hide(type) supportActionBar?.hide() } + val exoControls = it.findViewById(androidx.media3.ui.R.id.exo_bottom_bar) + if (it.isControllerFullyVisible) { + exoControls.getChildAt(0)?.visibility = View.VISIBLE + } else { + exoControls.getChildAt(0)?.visibility = View.INVISIBLE + } } ) it.player = videoPlayer @@ -684,16 +697,18 @@ class PreviewMediaActivity : } @Suppress("TooGenericExceptionCaught") + @IonosCustomization("Better UX") private fun playVideo() { setupVideoView() - if (file?.isDown == true) { - prepareVideoPlayer(file?.storageUri) - } else { - try { - LoadStreamUrl(this, user, clientFactory).execute(file?.localId) - } catch (e: Exception) { - Log_OC.e(TAG, "Loading stream url for Video not possible: $e") + if(videoPlayer?.currentMediaItem == null) { + if (file?.isDown == true) + prepareVideoPlayer(file?.storageUri) + } else { + try { + LoadStreamUrl(this, user, clientFactory).execute(file?.localId) + } catch (e: Exception) { + Log_OC.e(TAG, "Loading stream url for Video not possible: $e") } } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt index 91237ec84f63..ab6c50192fb5 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaPagerAdapter.kt @@ -14,6 +14,7 @@ import android.util.SparseArray import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.User import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.FileDataStorageManager @@ -45,6 +46,7 @@ class PreviewMediaPagerAdapter : FragmentStateAdapter { * @param parentFolder Folder where images will be searched for. * @param storageManager Bridge to database. */ + @IonosCustomization("Only images in folder pager") @Suppress("LongParameterList") constructor( fragmentActivity: FragmentActivity, @@ -60,7 +62,7 @@ class PreviewMediaPagerAdapter : FragmentStateAdapter { this.user = user this.selectedFile = selectedFile mStorageManager = storageManager - imageFiles = mStorageManager.getFolderImagesAndVideos(parentFolder, onlyOnDevice) + imageFiles = mStorageManager.getFolderImages(parentFolder, onlyOnDevice) val sortOrder = preferences.getSortOrderByFolder(parentFolder) val foldersBeforeFiles = preferences.isSortFoldersBeforeFiles() diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.kt index a381647ae034..168c247500fa 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.kt @@ -22,6 +22,7 @@ import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.ionos.annotation.IonosCustomization import com.nextcloud.android.lib.richWorkspace.RichWorkspaceDirectEditingRemoteOperation import com.owncloud.android.R import com.owncloud.android.ui.activity.FileActivity @@ -49,6 +50,7 @@ class PreviewTextStringFragment : PreviewTextFragment() { super.onSaveInstanceState(outState) } + @IonosCustomization("Removed FAB theming") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = super.onCreateView(inflater, container, savedInstanceState) ?: throw RuntimeException("View can not be null") @@ -58,7 +60,6 @@ class PreviewTextStringFragment : PreviewTextFragment() { isEnabled = true setOnClickListener { edit() } setImageResource(R.drawable.ic_edit) - viewThemeUtils.material.themeFAB(this) } addMenuProvider() diff --git a/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinFragment.kt b/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinFragment.kt index 87846a52b5b1..6d65ed021d7a 100644 --- a/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinFragment.kt @@ -20,7 +20,6 @@ import android.view.View import android.view.ViewGroup import android.widget.AbsListView import android.widget.PopupMenu -import android.widget.TextView import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat @@ -33,6 +32,7 @@ import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar +import com.ionos.annotation.IonosCustomization import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -312,10 +312,11 @@ class TrashbinFragment : navigatorActivity?.setDrawerIndicatorEnabled(isRoot) } + @IonosCustomization("Sort button icon") override fun onSortingOrderChosen(selection: FileSortOrder?) { val navigatorActivity = getTypedActivity(NavigatorActivity::class.java) - val sortButton = navigatorActivity?.findViewById(R.id.sort_button) - sortButton?.setText(DisplayUtils.getSortOrderStringId(selection)) + val sortButton = navigatorActivity?.findViewById(R.id.sort_button) + sortButton?.setIconResource(DisplayUtils.getSortOrderIconRes(selection)) trashbinListAdapter?.setSortOrder(selection) } diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index ff8fd1e9d408..e280012523bf 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -50,6 +50,9 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.google.android.material.snackbar.Snackbar; +import com.ionos.annotation.IonosCustomization; +import com.ionos.utils.IonosBuildHelper; +import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.OfflineOperationType; @@ -86,6 +89,7 @@ import java.util.Locale; import java.util.TimeZone; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -97,6 +101,7 @@ import androidx.fragment.app.FragmentTransaction; import static com.owncloud.android.ui.dialog.SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT; +import static com.owncloud.android.utils.FileSortOrder.SORT_A_TO_Z_ID; import static com.owncloud.android.utils.FileSortOrder.SORT_BIG_TO_SMALL_ID; import static com.owncloud.android.utils.FileSortOrder.SORT_NEW_TO_OLD_ID; import static com.owncloud.android.utils.FileSortOrder.SORT_OLD_TO_NEW_ID; @@ -414,6 +419,7 @@ public static void setAvatar(@NonNull User user, @NonNull String userId, AvatarG * @param callContext which context is called to set the generated avatar * @param context general context */ + @IonosCustomization public static void setAvatar(@NonNull User user, @NonNull String userId, String displayName, @@ -450,6 +456,12 @@ public static void setAvatar(@NonNull User user, v.setContentDescription(String.valueOf(user.toPlatformAccount().hashCode())); } + if (IonosBuildHelper.isIonosBuild()) { + Drawable avatar = ResourcesCompat.getDrawable(resources, R.drawable.account_circle_white, null); + listener.avatarGenerated(avatar, callContext); + return; + } + final String accountName = user.getAccountName(); String serverName = accountName.substring(accountName.lastIndexOf('@') + 1); Drawable avatar; @@ -608,6 +620,54 @@ public static void showSnackMessage(View view, String message) { snackbar.show(); }); } + + /** + * Shows a Snackbar with an action button to open app settings. + * + * @param view The view to find a parent from + * @param message The message string + * @param context Context to start the settings intent + */ + @IonosCustomization("SnackMessageWithSettingsAction") + public static void showSnackMessageWithSettingsAction(View view, + String message, + Context context) { + if (view == null || context == null) { + Log_OC.e(TAG, "snackbar cannot be shown - view or context is null"); + return; + } + + mainLooper.post(() -> { + final Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); + snackbar.setAction(R.string.settings_action, v -> openAppSettings(context)); + snackbar.show(); + }); + } + + /** + * Opens the app settings screen where user can grant permissions. + * + * @param context Context to start the settings activity + */ + @IonosCustomization("openAppSettings") + private static void openAppSettings(Context context) { + try { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", context.getPackageName(), null); + intent.setData(uri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception e) { + Log_OC.e(TAG, "Error opening app settings", e); + try { + Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception ex) { + Log_OC.e(TAG, "Error opening settings", ex); + } + } + } // endregion private static View findFABView(Activity activity) { @@ -723,6 +783,25 @@ static public void openSortingOrderDialogFragment(FragmentManager supportFragmen }; } + @IonosCustomization + public static @DrawableRes int getSortOrderIconRes(FileSortOrder sortOrder) { + switch (sortOrder.name) { + case SORT_Z_TO_A_ID: + return R.drawable.ic_alphabetical_desc; + case SORT_NEW_TO_OLD_ID: + return R.drawable.ic_modification_desc; + case SORT_OLD_TO_NEW_ID: + return R.drawable.ic_modification_asc; + case SORT_BIG_TO_SMALL_ID: + return R.drawable.ic_size_desc; + case SORT_SMALL_TO_BIG_ID: + return R.drawable.ic_size_asc; + case SORT_A_TO_Z_ID: + default: + return R.drawable.ic_alphabetical_asc; + } + } + public static String getDateByPattern(long timestamp, String pattern) { return getDateByPattern(timestamp, null, pattern); } diff --git a/app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt b/app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt index 119f5e5f7182..066107951d60 100644 --- a/app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt @@ -12,10 +12,12 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable +import android.view.Gravity import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.scale +import com.ionos.annotation.IonosCustomization object DrawableUtil { @@ -25,6 +27,15 @@ object DrawableUtil { return drawable } + @IonosCustomization + fun addDrawableAsOverlay(backgroundDrawable: Drawable, overlayDrawable: Drawable): LayerDrawable { + + return LayerDrawable(arrayOf(backgroundDrawable, overlayDrawable)).apply { + setLayerSize(1, overlayDrawable.intrinsicWidth, overlayDrawable.intrinsicHeight) + setLayerGravity(1, Gravity.CENTER) + } + } + fun getResizedDrawable(context: Context, drawable: Drawable, pxSize: Int): Drawable { if (drawable is BitmapDrawable) { val originalBitmap = drawable.bitmap @@ -38,30 +49,5 @@ object DrawableUtil { drawable.draw(canvas) return bitmap.toDrawable(context.resources) - } - - fun addDrawableAsOverlay(backgroundDrawable: Drawable, overlayDrawable: Drawable): LayerDrawable { - val containerDrawable = LayerDrawable(arrayOf(backgroundDrawable, overlayDrawable)) - - val overlayWidth = overlayDrawable.intrinsicWidth - val overlayHeight = overlayDrawable.intrinsicHeight - val backgroundWidth = backgroundDrawable.intrinsicWidth - val backgroundHeight = backgroundDrawable.intrinsicHeight - - val scaleFactor = 2f / maxOf(overlayWidth, overlayHeight) - val scaledOverlayWidth = (overlayWidth * scaleFactor).toInt() - val scaledOverlayHeight = (overlayHeight * scaleFactor).toInt() - - val left = (backgroundWidth - scaledOverlayWidth) / 2 - val top = (backgroundHeight - scaledOverlayHeight) / 2 - - // Icons are centered on the folder icon. However, some icons take up more vertical space, - // so adding a top margin to all icons helps center the overlay icon better. - val topMargin = 2 - - containerDrawable.setLayerInset(1, left, top + topMargin, left, top) - (overlayDrawable as? BitmapDrawable)?.setBounds(0, 0, scaledOverlayWidth, scaledOverlayHeight) - - return containerDrawable - } + } } diff --git a/app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java b/app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java index 2e8e56e3bf51..e679fdc73693 100644 --- a/app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java +++ b/app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java @@ -10,6 +10,7 @@ import android.content.res.Resources; import android.view.Menu; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.client.account.User; import com.owncloud.android.R; import com.owncloud.android.lib.resources.status.OCCapability; @@ -72,6 +73,11 @@ public static void setupHomeMenuItem(Menu menu, Resources resources) { } } + @IonosCustomization + public static void removePersonalFiles(Menu menu) { + removeMenuItem(menu, R.id.nav_personal_files); + } + private static void removeMenuItem(Menu menu, int... menuIds) { if (menuIds != null) { for (int menuId : menuIds) { diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 432cef709305..328b5b37c336 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -13,6 +13,7 @@ import android.content.Context import androidx.annotation.VisibleForTesting import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManagerImpl import com.nextcloud.utils.CmsSignatureVerifier import com.nextcloud.utils.autoRename.AutoRename import com.nextcloud.utils.e2ee.E2EVersionHelper diff --git a/app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java b/app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java index 3fe26e7d0e63..3f668208ae47 100644 --- a/app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java +++ b/app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java @@ -16,6 +16,7 @@ import android.net.Uri; import android.webkit.MimeTypeMap; +import com.ionos.annotation.IonosCustomization; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; @@ -88,6 +89,7 @@ private MimeTypeUtil() { * @param filename Name, with extension. * @return Drawable of an image resource. */ + @IonosCustomization("Custom icon tint") public static Drawable getFileTypeIcon(String mimetype, String filename, Context context, @@ -100,9 +102,9 @@ public static Drawable getFileTypeIcon(String mimetype, } icon = icon.mutate(); - - if (R.drawable.file_zip == iconId) { - viewThemeUtils.platform.tintDrawable(context, icon, ColorRole.PRIMARY); + + if (R.drawable.file_image != iconId && R.drawable.file_movie != iconId && R.drawable.file_sound != iconId) { + icon.setTint(context.getColor(R.color.filelist_file_icon_color)); } return icon; @@ -146,14 +148,16 @@ public static int getFileTypeIconId(String mimetype, String filename) { return determineIconIdByMimeTypeList(possibleMimeTypes); } + @IonosCustomization("Custom icon tint") public static Drawable getDefaultFolderIcon(Context context, ViewThemeUtils viewThemeUtils) { Drawable drawable = ContextCompat.getDrawable(context, R.drawable.folder); assert(drawable != null); - viewThemeUtils.platform.tintDrawable(context, drawable, ColorRole.PRIMARY); + drawable.setTint(context.getColor(R.color.filelist_file_icon_color)); return drawable; } + @IonosCustomization("Remove custom overlay color for dark mode") public static LayerDrawable getFolderIcon(boolean isDarkModeActive, Integer overlayIconId, Context context, ViewThemeUtils viewThemeUtils) { Drawable folderDrawable = getDefaultFolderIcon(context, viewThemeUtils); assert(folderDrawable != null); @@ -167,10 +171,6 @@ public static LayerDrawable getFolderIcon(boolean isDarkModeActive, Integer over Drawable overlayDrawable = ContextCompat.getDrawable(context, overlayIconId); assert(overlayDrawable != null); - if (isDarkModeActive) { - overlayDrawable = DrawableUtil.INSTANCE.changeColor(overlayDrawable, R.color.dark); - } - return DrawableUtil.INSTANCE.addDrawableAsOverlay(folderDrawable, overlayDrawable); } diff --git a/app/src/main/java/com/owncloud/android/utils/theme/ViewThemeUtils.kt b/app/src/main/java/com/owncloud/android/utils/theme/ViewThemeUtils.kt index a16b9b057259..7dac425518a3 100644 --- a/app/src/main/java/com/owncloud/android/utils/theme/ViewThemeUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/theme/ViewThemeUtils.kt @@ -7,6 +7,12 @@ */ package com.owncloud.android.utils.theme +import com.ionos.annotation.IonosCustomization +import com.ionos.utils.IonosAndroidViewThemeUtils +import com.ionos.utils.IonosAndroidXViewThemeUtils +import com.ionos.utils.IonosDialogViewThemeUtils +import com.ionos.utils.IonosFilesSpecificViewThemeUtils +import com.ionos.utils.IonosMaterialViewThemeUtils import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.theme.MaterialSchemes import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase @@ -19,21 +25,31 @@ import javax.inject.Inject /** * Child fields intentionally constructed instead of injected in order to reuse schemes for performance */ -class ViewThemeUtils @Inject constructor(schemes: MaterialSchemes, colorUtil: ColorUtil) : ViewThemeUtilsBase(schemes) { +@IonosCustomization +class ViewThemeUtils @Inject constructor( + schemes: MaterialSchemes, + colorUtil: ColorUtil +) : ViewThemeUtilsBase(schemes) { + private val platformDelegate = AndroidViewThemeUtils(schemes, colorUtil) + private val androidxDelegate = AndroidXViewThemeUtils(schemes, platformDelegate) + @JvmField - val platform = AndroidViewThemeUtils(schemes, colorUtil) + val platform = IonosAndroidViewThemeUtils(platformDelegate) @JvmField - val material = MaterialViewThemeUtils(schemes, colorUtil) + val material = IonosMaterialViewThemeUtils(MaterialViewThemeUtils(schemes, colorUtil)) @JvmField - val androidx = AndroidXViewThemeUtils(schemes, platform) + val androidx = IonosAndroidXViewThemeUtils(androidxDelegate) @JvmField - val dialog = DialogViewThemeUtils(schemes) + val dialog = IonosDialogViewThemeUtils(DialogViewThemeUtils(schemes)) @JvmField - val files = FilesSpecificViewThemeUtils(schemes, colorUtil, platform, androidx) + val files = IonosFilesSpecificViewThemeUtils( + schemes, + FilesSpecificViewThemeUtils(schemes, colorUtil, platformDelegate, androidxDelegate), + ) class Factory @Inject constructor( private val schemesProvider: MaterialSchemesProvider, diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 000000000000..a21744ff2499 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 000000000000..a2b5526cb1ec --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/color-night/radio_button_color.xml b/app/src/main/res/color-night/radio_button_color.xml new file mode 100644 index 000000000000..b9f2ffa47bb3 --- /dev/null +++ b/app/src/main/res/color-night/radio_button_color.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/fab_background_tint.xml b/app/src/main/res/color/fab_background_tint.xml new file mode 100644 index 000000000000..debb631f48e9 --- /dev/null +++ b/app/src/main/res/color/fab_background_tint.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/main/res/color/filled_button_bg_color.xml b/app/src/main/res/color/filled_button_bg_color.xml new file mode 100644 index 000000000000..518c975d874c --- /dev/null +++ b/app/src/main/res/color/filled_button_bg_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/color/filled_button_text_color.xml b/app/src/main/res/color/filled_button_text_color.xml new file mode 100644 index 000000000000..6eef0fe45d18 --- /dev/null +++ b/app/src/main/res/color/filled_button_text_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/color/outlined_button_bg_color.xml b/app/src/main/res/color/outlined_button_bg_color.xml new file mode 100644 index 000000000000..3f66ee2be899 --- /dev/null +++ b/app/src/main/res/color/outlined_button_bg_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/color/outlined_button_stroke_color.xml b/app/src/main/res/color/outlined_button_stroke_color.xml new file mode 100644 index 000000000000..0c25161bc46c --- /dev/null +++ b/app/src/main/res/color/outlined_button_stroke_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/color/outlined_button_text_color.xml b/app/src/main/res/color/outlined_button_text_color.xml new file mode 100644 index 000000000000..2140ebe47a18 --- /dev/null +++ b/app/src/main/res/color/outlined_button_text_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/color/radio_button_color.xml b/app/src/main/res/color/radio_button_color.xml new file mode 100644 index 000000000000..630ce8d2dfa6 --- /dev/null +++ b/app/src/main/res/color/radio_button_color.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text_button_text_color.xml b/app/src/main/res/color/text_button_text_color.xml new file mode 100644 index 000000000000..c40c98ad77f3 --- /dev/null +++ b/app/src/main/res/color/text_button_text_color.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-v33/player_ic_notification_audio.xml b/app/src/main/res/drawable-v33/player_ic_notification_audio.xml new file mode 100644 index 000000000000..ef8c993797fd --- /dev/null +++ b/app/src/main/res/drawable-v33/player_ic_notification_audio.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-v33/player_ic_notification_video.xml b/app/src/main/res/drawable-v33/player_ic_notification_video.xml new file mode 100644 index 000000000000..12c140c6c1ca --- /dev/null +++ b/app/src/main/res/drawable-v33/player_ic_notification_video.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml new file mode 100644 index 000000000000..e27a0271888a --- /dev/null +++ b/app/src/main/res/drawable/background_splash.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/file_pdf.xml b/app/src/main/res/drawable/file_pdf.xml index af485a0487b2..e7510ce80fec 100644 --- a/app/src/main/res/drawable/file_pdf.xml +++ b/app/src/main/res/drawable/file_pdf.xml @@ -5,13 +5,15 @@ ~ SPDX-License-Identifier: Apache-2.0 --> diff --git a/app/src/main/res/drawable/grid_mode_item_background.xml b/app/src/main/res/drawable/grid_mode_item_background.xml new file mode 100644 index 000000000000..a35817881607 --- /dev/null +++ b/app/src/main/res/drawable/grid_mode_item_background.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/grid_mode_selected_item_background.xml b/app/src/main/res/drawable/grid_mode_selected_item_background.xml new file mode 100644 index 000000000000..88b9877084b8 --- /dev/null +++ b/app/src/main/res/drawable/grid_mode_selected_item_background.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_data_protection.xml b/app/src/main/res/drawable/ic_data_protection.xml new file mode 100644 index 000000000000..72c7887423de --- /dev/null +++ b/app/src/main/res/drawable/ic_data_protection.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_cancel_sync.xml b/app/src/main/res/drawable/ic_file_action_cancel_sync.xml new file mode 100644 index 000000000000..c7f0db0ca5d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_cancel_sync.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_edit.xml b/app/src/main/res/drawable/ic_file_action_edit.xml new file mode 100644 index 000000000000..e2bcc65afb5c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_edit.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_lock_file.xml b/app/src/main/res/drawable/ic_file_action_lock_file.xml new file mode 100644 index 000000000000..eaeef744157c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_lock_file.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_move_or_copy.xml b/app/src/main/res/drawable/ic_file_action_move_or_copy.xml new file mode 100644 index 000000000000..388986ce246c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_move_or_copy.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_open_file_with.xml b/app/src/main/res/drawable/ic_file_action_open_file_with.xml new file mode 100644 index 000000000000..388986ce246c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_open_file_with.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_set_encrypted.xml b/app/src/main/res/drawable/ic_file_action_set_encrypted.xml new file mode 100644 index 000000000000..b7d8b3c97021 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_set_encrypted.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_stream_media.xml b/app/src/main/res/drawable/ic_file_action_stream_media.xml new file mode 100644 index 000000000000..be328e566a66 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_stream_media.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_sync_file.xml b/app/src/main/res/drawable/ic_file_action_sync_file.xml new file mode 100644 index 000000000000..7054f44c69be --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_sync_file.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_unlock_file.xml b/app/src/main/res/drawable/ic_file_action_unlock_file.xml new file mode 100644 index 000000000000..8b84787751ec --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_unlock_file.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_action_unset_encrypted.xml b/app/src/main/res/drawable/ic_file_action_unset_encrypted.xml new file mode 100644 index 000000000000..9a17c0e2b9dd --- /dev/null +++ b/app/src/main/res/drawable/ic_file_action_unset_encrypted.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_folder_all_share_types.xml b/app/src/main/res/drawable/ic_folder_all_share_types.xml new file mode 100644 index 000000000000..d892b5fde64a --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_all_share_types.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_shared_all_types.xml b/app/src/main/res/drawable/ic_shared_all_types.xml new file mode 100644 index 000000000000..d7f80cca9183 --- /dev/null +++ b/app/src/main/res/drawable/ic_shared_all_types.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_audio.xml b/app/src/main/res/drawable/player_ic_audio.xml new file mode 100644 index 000000000000..96cfb9d2a11f --- /dev/null +++ b/app/src/main/res/drawable/player_ic_audio.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_close.xml b/app/src/main/res/drawable/player_ic_close.xml new file mode 100644 index 000000000000..ee43a7f8a820 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_close.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_notification_audio.xml b/app/src/main/res/drawable/player_ic_notification_audio.xml new file mode 100644 index 000000000000..aec4a2da0a57 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_notification_audio.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/player_ic_notification_video.xml b/app/src/main/res/drawable/player_ic_notification_video.xml new file mode 100644 index 000000000000..0eb1d0d71bce --- /dev/null +++ b/app/src/main/res/drawable/player_ic_notification_video.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/player_ic_pause.xml b/app/src/main/res/drawable/player_ic_pause.xml new file mode 100644 index 000000000000..0caa2063b795 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_pause.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/player_ic_play.xml b/app/src/main/res/drawable/player_ic_play.xml new file mode 100644 index 000000000000..593cc62cf301 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_play.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/player_ic_repeat.xml b/app/src/main/res/drawable/player_ic_repeat.xml new file mode 100644 index 000000000000..933ce5c46673 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_repeat.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_shuffle.xml b/app/src/main/res/drawable/player_ic_shuffle.xml new file mode 100644 index 000000000000..fdba83301593 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_shuffle.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_skip_next.xml b/app/src/main/res/drawable/player_ic_skip_next.xml new file mode 100644 index 000000000000..7f56ee4c0ead --- /dev/null +++ b/app/src/main/res/drawable/player_ic_skip_next.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_skip_previous.xml b/app/src/main/res/drawable/player_ic_skip_previous.xml new file mode 100644 index 000000000000..7259194fd868 --- /dev/null +++ b/app/src/main/res/drawable/player_ic_skip_previous.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_ic_video.xml b/app/src/main/res/drawable/player_ic_video.xml new file mode 100644 index 000000000000..ca3ca3a8a6ba --- /dev/null +++ b/app/src/main/res/drawable/player_ic_video.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/player_progress_drawable.xml b/app/src/main/res/drawable/player_progress_drawable.xml new file mode 100644 index 000000000000..ce05f0fa28c2 --- /dev/null +++ b/app/src/main/res/drawable/player_progress_drawable.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_progress_thumb.xml b/app/src/main/res/drawable/player_progress_thumb.xml new file mode 100644 index 000000000000..13b1910c2ac6 --- /dev/null +++ b/app/src/main/res/drawable/player_progress_thumb.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_rect_4dp.xml b/app/src/main/res/drawable/rounded_rect_4dp.xml new file mode 100644 index 000000000000..1952e3c9f856 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rect_4dp.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout-land/account_setup.xml b/app/src/main/res/layout-land/account_setup.xml index 8fe6b05b3035..c75a890fdba1 100644 --- a/app/src/main/res/layout-land/account_setup.xml +++ b/app/src/main/res/layout-land/account_setup.xml @@ -14,7 +14,8 @@ android:layout_height="match_parent" android:layout_alignParentTop="true" android:orientation="horizontal" - android:padding="@dimen/standard_padding"> + android:padding="@dimen/standard_padding" + android:visibility="invisible"> diff --git a/app/src/main/res/layout/account_removal_dialog.xml b/app/src/main/res/layout/account_removal_dialog.xml index 4fcf4c5935da..d0dd0d6fc1f9 100644 --- a/app/src/main/res/layout/account_removal_dialog.xml +++ b/app/src/main/res/layout/account_removal_dialog.xml @@ -10,6 +10,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> + + + + + + + + + diff --git a/app/src/main/res/layout/activity_data_protection_detail_page.xml b/app/src/main/res/layout/activity_data_protection_detail_page.xml new file mode 100644 index 000000000000..459f10840155 --- /dev/null +++ b/app/src/main/res/layout/activity_data_protection_detail_page.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_data_protection_overview_page.xml b/app/src/main/res/layout/activity_data_protection_overview_page.xml new file mode 100644 index 000000000000..7363d67193dd --- /dev/null +++ b/app/src/main/res/layout/activity_data_protection_overview_page.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_edit_image.xml b/app/src/main/res/layout/activity_edit_image.xml index 097431adc99d..08e9da12ce8e 100644 --- a/app/src/main/res/layout/activity_edit_image.xml +++ b/app/src/main/res/layout/activity_edit_image.xml @@ -11,14 +11,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" - android:background="@color/black" + android:background="@color/edit_image_background" tools:ignore="Overdraw"> + tools:text="@string/placeholder_filename_with_extension" /> diff --git a/app/src/main/res/layout/activity_privacy_settings.xml b/app/src/main/res/layout/activity_privacy_settings.xml new file mode 100644 index 000000000000..3ab75699d610 --- /dev/null +++ b/app/src/main/res/layout/activity_privacy_settings.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/deep_link_login.xml b/app/src/main/res/layout/deep_link_login.xml index 5212f9f0df34..7617c430b3aa 100644 --- a/app/src/main/res/layout/deep_link_login.xml +++ b/app/src/main/res/layout/deep_link_login.xml @@ -33,7 +33,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="middle" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="@color/text_color" android:textSize="20sp" android:textStyle="bold" /> diff --git a/app/src/main/res/layout/dialog_app_passcode.xml b/app/src/main/res/layout/dialog_app_passcode.xml index 7f530e021170..6ee4b02157f5 100644 --- a/app/src/main/res/layout/dialog_app_passcode.xml +++ b/app/src/main/res/layout/dialog_app_passcode.xml @@ -7,6 +7,7 @@ diff --git a/app/src/main/res/layout/dialog_choose_account.xml b/app/src/main/res/layout/dialog_choose_account.xml index 1efda6b4e0ba..901449ec5621 100644 --- a/app/src/main/res/layout/dialog_choose_account.xml +++ b/app/src/main/res/layout/dialog_choose_account.xml @@ -68,10 +68,12 @@ android:textAlignment="textStart" android:textAllCaps="false" android:textColor="@color/fontAppbar" - app:icon="@drawable/chat_bubble" + android:visibility="gone" + app:icon="@drawable/ic_edit" app:iconGravity="start" - app:iconPadding="@dimen/standard_half_padding" - app:iconTint="@color/fontAppbar" /> + app:iconPadding="22dp" + app:iconTint="@color/fontAppbar" + app:ionosCustomization="" /> diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index 88f7ade88c4a..4859c05a0de2 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -12,7 +12,7 @@ android:id="@+id/drawer_header_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/primary" + android:background="@color/drawer_header_background" android:gravity="center" android:orientation="vertical"> diff --git a/app/src/main/res/layout/file_details_fragment.xml b/app/src/main/res/layout/file_details_fragment.xml index aac508c36f68..1cc7172bd726 100644 --- a/app/src/main/res/layout/file_details_fragment.xml +++ b/app/src/main/res/layout/file_details_fragment.xml @@ -34,11 +34,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="middle" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="@color/text_color" android:textSize="20sp" android:textStyle="bold" - tools:text="@string/placeholder_filename" /> + tools:text="@string/placeholder_filename_with_extension" /> + android:scaleType="centerInside" + android:src="@drawable/account_circle_white" /> diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index 14a056da8f5c..7f4740d6f5b7 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -40,7 +40,7 @@ android:layout_width="@dimen/user_icon_size" android:layout_height="@dimen/user_icon_size" android:contentDescription="@string/avatar" - android:src="@drawable/ic_user_outline" /> + android:src="@drawable/ic_user" /> @@ -55,12 +57,15 @@ android:id="@+id/can_edit_radio_button" android:layout_width="match_parent" android:layout_height="wrap_content" + app:buttonTint="@color/radio_button_color" + android:textColor="@color/text_color" android:minHeight="@dimen/minimum_size_for_touchable_area" android:text="@string/share_permission_can_edit" /> @@ -81,8 +88,9 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/standard_margin" android:minHeight="@dimen/minimum_size_for_touchable_area" + android:text="@string/allow_resharing" + android:textColor="@color/text_color" android:visibility="gone" - android:text="@string/share_allow_download_and_sync_permission" tools:visibility="visible" /> + android:text="@string/share_no_password_title" + android:textColor="@color/text_color" /> + android:text="@string/share_no_expiration_date_label" + android:textColor="@color/text_color" /> + android:text="@string/link_name" + android:textColor="@color/text_color" /> + app:cornerRadius="@dimen/button_corner_radius" + app:ionosCustomization="Adjust the height to match the height of the Next button" /> + android:theme="@style/Theme.Dialog.IconButton.Filled" + android:textColor="@color/filled_button_text_color" + app:cornerRadius="@dimen/button_corner_radius" + app:ionosCustomization="Limit max lines number to 2" /> diff --git a/app/src/main/res/layout/file_thumbnail.xml b/app/src/main/res/layout/file_thumbnail.xml index 6f318d47e9b3..d143e9c1b661 100644 --- a/app/src/main/res/layout/file_thumbnail.xml +++ b/app/src/main/res/layout/file_thumbnail.xml @@ -28,9 +28,10 @@ diff --git a/app/src/main/res/layout/files_folder_picker.xml b/app/src/main/res/layout/files_folder_picker.xml index f1d4f53ebb5d..ddbdc84a720e 100644 --- a/app/src/main/res/layout/files_folder_picker.xml +++ b/app/src/main/res/layout/files_folder_picker.xml @@ -51,7 +51,7 @@ @@ -31,6 +32,8 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/standard_margin" android:layout_marginBottom="@dimen/standard_margin" + android:textStyle="bold" + app:ionosCustomization="bold text" android:textAppearance="?android:attr/textAppearanceLarge" tools:text="@string/placeholder_year" /> diff --git a/app/src/main/res/layout/grid_item.xml b/app/src/main/res/layout/grid_item.xml index ddbbc8c627c3..5b1038c6982a 100644 --- a/app/src/main/res/layout/grid_item.xml +++ b/app/src/main/res/layout/grid_item.xml @@ -185,6 +185,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml index 8717f2ffc7aa..e6688ae84264 100644 --- a/app/src/main/res/layout/list_item.xml +++ b/app/src/main/res/layout/list_item.xml @@ -225,6 +225,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_audio_file_fragment.xml b/app/src/main/res/layout/player_audio_file_fragment.xml new file mode 100644 index 000000000000..f1eddb8cc50e --- /dev/null +++ b/app/src/main/res/layout/player_audio_file_fragment.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_audio_view.xml b/app/src/main/res/layout/player_audio_view.xml new file mode 100644 index 000000000000..af114e694258 --- /dev/null +++ b/app/src/main/res/layout/player_audio_view.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_control_view.xml b/app/src/main/res/layout/player_control_view.xml new file mode 100644 index 000000000000..d12e0685db0d --- /dev/null +++ b/app/src/main/res/layout/player_control_view.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_pager.xml b/app/src/main/res/layout/player_pager.xml new file mode 100644 index 000000000000..6f440fc8becc --- /dev/null +++ b/app/src/main/res/layout/player_pager.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_video_file_fragment.xml b/app/src/main/res/layout/player_video_file_fragment.xml new file mode 100644 index 000000000000..800c1dcc8ff2 --- /dev/null +++ b/app/src/main/res/layout/player_video_file_fragment.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_video_view.xml b/app/src/main/res/layout/player_video_view.xml new file mode 100644 index 000000000000..209764136693 --- /dev/null +++ b/app/src/main/res/layout/player_video_view.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/richdocuments_webview.xml b/app/src/main/res/layout/richdocuments_webview.xml index 5e4e6bb7cd88..52869bcd69cb 100644 --- a/app/src/main/res/layout/richdocuments_webview.xml +++ b/app/src/main/res/layout/richdocuments_webview.xml @@ -32,7 +32,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="middle" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="@color/text_color" android:textSize="20sp" android:textStyle="bold"/> diff --git a/app/src/main/res/layout/send_share_fragment.xml b/app/src/main/res/layout/send_share_fragment.xml index 12342895bc50..2fe500af1d77 100644 --- a/app/src/main/res/layout/send_share_fragment.xml +++ b/app/src/main/res/layout/send_share_fragment.xml @@ -42,7 +42,7 @@ android:layout_height="wrap_content" android:text="@string/share" app:iconPadding="@dimen/standard_half_padding" - app:icon="@drawable/shared_via_users" /> + app:icon="@drawable/ic_share" /> + app:icon="@drawable/ic_link" /> diff --git a/app/src/main/res/layout/settings_privacy_switchers.xml b/app/src/main/res/layout/settings_privacy_switchers.xml new file mode 100644 index 000000000000..8aa656803d02 --- /dev/null +++ b/app/src/main/res/layout/settings_privacy_switchers.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml b/app/src/main/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml new file mode 100644 index 000000000000..88b1e407a71c --- /dev/null +++ b/app/src/main/res/layout/simple_file_list_actions_bottom_sheet_fragment.xml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/simple_spinner_item.xml b/app/src/main/res/layout/simple_spinner_item.xml new file mode 100644 index 000000000000..accb287cbdde --- /dev/null +++ b/app/src/main/res/layout/simple_spinner_item.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/layout/storage_path_item.xml b/app/src/main/res/layout/storage_path_item.xml index 44180c9acb64..932aaf985f08 100644 --- a/app/src/main/res/layout/storage_path_item.xml +++ b/app/src/main/res/layout/storage_path_item.xml @@ -15,6 +15,9 @@ android:gravity="center|start" android:paddingBottom="@dimen/standard_half_padding" android:text="@string/menu_item_sort_by_name_z_a" - app:icon="@drawable/ic_user_outline" + android:textColor="@color/text_color" + app:icon="@drawable/ic_user" app:iconPadding="@dimen/standard_padding" + app:iconTint="@color/default_icon_color" + app:ionosCustomization="Custom color" tools:text="@string/placeholder_file_path" /> diff --git a/app/src/main/res/layout/synced_folders_settings_layout.xml b/app/src/main/res/layout/synced_folders_settings_layout.xml index c8850c4e020c..2ee192fa8675 100644 --- a/app/src/main/res/layout/synced_folders_settings_layout.xml +++ b/app/src/main/res/layout/synced_folders_settings_layout.xml @@ -53,6 +53,8 @@ @@ -417,7 +419,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="?android:attr/textColorSecondary" /> @@ -431,7 +433,7 @@ @@ -454,7 +456,7 @@ android:id="@+id/btnPositive" android:layout_width="wrap_content" android:text="@string/common_save" - style="@style/Widget.Material3.Button.TonalButton" + style="@style/Theme.Dialog.IconButton.Filled.Tonal" android:layout_height="wrap_content" android:layout_weight="1"/> diff --git a/app/src/main/res/layout/trashbin_item.xml b/app/src/main/res/layout/trashbin_item.xml index 4d248ffd8fb5..282da65f0d6c 100644 --- a/app/src/main/res/layout/trashbin_item.xml +++ b/app/src/main/res/layout/trashbin_item.xml @@ -47,7 +47,7 @@ android:singleLine="true" android:textColor="@color/text_color" android:textSize="@dimen/two_line_primary_text_size" - tools:text="@string/placeholder_filename" /> + tools:text="@string/placeholder_filename_with_extension" /> diff --git a/app/src/main/res/layout/unified_search_current_directory_item.xml b/app/src/main/res/layout/unified_search_current_directory_item.xml index 5b5c8d6e6533..dfc008d2578e 100644 --- a/app/src/main/res/layout/unified_search_current_directory_item.xml +++ b/app/src/main/res/layout/unified_search_current_directory_item.xml @@ -38,6 +38,20 @@ + + + + diff --git a/app/src/main/res/layout/upload_list_item.xml b/app/src/main/res/layout/upload_list_item.xml index db6acf802b0d..55dca7af85d6 100755 --- a/app/src/main/res/layout/upload_list_item.xml +++ b/app/src/main/res/layout/upload_list_item.xml @@ -48,7 +48,7 @@ android:layout_gravity="center_vertical" android:ellipsize="middle" android:singleLine="true" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="@color/text_color" android:textSize="@dimen/two_line_primary_text_size" /> diff --git a/app/src/main/res/layout/uploader_list_item_layout.xml b/app/src/main/res/layout/uploader_list_item_layout.xml index f9af976e120b..37bd3a0e3d6a 100644 --- a/app/src/main/res/layout/uploader_list_item_layout.xml +++ b/app/src/main/res/layout/uploader_list_item_layout.xml @@ -38,7 +38,7 @@ android:layout_marginEnd="@dimen/standard_quarter_margin" android:ellipsize="middle" android:singleLine="true" - android:text="@string/placeholder_filename" + android:text="@string/placeholder_filename_with_extension" android:textColor="@color/text_color" android:textSize="@dimen/two_line_primary_text_size"/> diff --git a/app/src/main/res/layout/user_info_layout.xml b/app/src/main/res/layout/user_info_layout.xml index a7cac58fdad3..34e4eeba9107 100644 --- a/app/src/main/res/layout/user_info_layout.xml +++ b/app/src/main/res/layout/user_info_layout.xml @@ -62,6 +62,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/userinfo_icon" app:layout_constraintTop_toTopOf="@id/userinfo_icon" + app:layout_constraintBottom_toTopOf="@id/userinfo_username" tools:text="@string/placeholder_first_name_last_name" /> diff --git a/app/src/main/res/layout/view_authorization_method.xml b/app/src/main/res/layout/view_authorization_method.xml new file mode 100644 index 000000000000..51ea5477db49 --- /dev/null +++ b/app/src/main/res/layout/view_authorization_method.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_folder_picker.xml b/app/src/main/res/menu/activity_folder_picker.xml index 3e6fdcebbf2a..21a6ab87f424 100644 --- a/app/src/main/res/menu/activity_folder_picker.xml +++ b/app/src/main/res/menu/activity_folder_picker.xml @@ -23,6 +23,6 @@ android:icon="@drawable/ic_action_create_dir" android:orderInCategory="1" android:title="@string/actionbar_mkdir" - app:iconTint="?attr/colorOnSurface" + app:ionosCustomization="Remove tint" app:showAsAction="ifRoom"/> diff --git a/app/src/main/res/menu/activity_receive_external_files.xml b/app/src/main/res/menu/activity_receive_external_files.xml index 4c22d95237bd..ed4a7894823e 100644 --- a/app/src/main/res/menu/activity_receive_external_files.xml +++ b/app/src/main/res/menu/activity_receive_external_files.xml @@ -27,7 +27,7 @@ android:icon="@drawable/ic_action_create_dir" android:orderInCategory="1" android:title="@string/actionbar_mkdir" - app:iconTint="?attr/colorOnSurface" + app:ionosCustomization="Remove tint" app:showAsAction="never"/> diff --git a/app/src/main/res/values-de/ionos-strings.xml b/app/src/main/res/values-de/ionos-strings.xml new file mode 100644 index 000000000000..8bf223325013 --- /dev/null +++ b/app/src/main/res/values-de/ionos-strings.xml @@ -0,0 +1,35 @@ + + + + + Sie müssen sich über den Browser anmelden + Willkommen in ihrem Cloud-Speicher + Anmelden + Abbrechen + Wiederholen + Zustimmen + Diese Anwendung verwendet Cookies und ähnliche Technologien. Durch Klicken auf Zustimmen akzeptieren Sie die Verarbeitung und auch die Weitergabe Ihrer Daten an Dritte. Weitere Informationen, auch zur Datenverarbeitung durch Drittanbieter, finden Sie in den Einstellungen und in unserer Datenschutzhinweise Sie können die Nutzung der Tools Ablehnen oder Ihre Auswahl jederzeit über Ihre Einstellungen anpassen. + Diese Daten helfen uns, die App-Nutzung für Sie zu optimieren und Systemabstürze und Fehler schneller zu identifizieren. + Analyse der Datenerfassung zur bedarfsgerechten Gestaltung + Einstellungen + Die Erfassung dieser Daten ist notwendig, um wesentliche Funktionen der App nutzen zu können. + Erforderliche Datenerfassung + Zur Optimierung unserer App erfassen wir anonymisierte Daten. Hierzu nutzen wir Softwarelösungen verschiedener Partner. Wir möchten Ihnen volle Transparenz und Entscheidungsgewalt über die Verarbeitung und Erfassung Ihrer anonymisierten Nutzungsdaten geben. Ihre Einstellungen können Sie auch später jederzeit unter dem Menüpunkt Datenschutz ändern. Bitte beachten Sie jedoch, dass die Datenerfassung einen erheblichen Beitrag zur Optimierung dieser App leistet und Sie diese Optimierungen durch die Unterbindung der Datenübermittlung verhindern. + Einstellungen speichern + Datenschutz-Einstellungen + Berechtigungen + Berechtigungen + %1$s braucht die Erlaubnis, auf Ihre Dateien zuzugreifen, um besser zu funktionieren. Sie können den Zugriff erlauben oder verweigern. + %1$s braucht die Erlaubnis, auf Ihre Dateien zuzugreifen, um besser zu funktionieren. Sie können den Zugriff erlauben oder verweigern. + Name oder E-Mail Adresse... + Verwendeter Speicherplatz + Filter + IONOS HiDrive Next benötigt die Berechtigung zum Zugriff auf die Kamera. + EINSTELLUNGEN + Versuche Anmeldung … + diff --git a/app/src/main/res/values-es-rCR/strings.xml b/app/src/main/res/values-es-rCR/strings.xml new file mode 100644 index 000000000000..a546e7599eee --- /dev/null +++ b/app/src/main/res/values-es-rCR/strings.xml @@ -0,0 +1,535 @@ + + + %1$s aplicación Android + Acerca de + versión %1$s + Ícono de la cuenta + ¡No se encontró la cuenta! + Editar + Enviar/Compartir + Vista de cuadrícula + Vista de lista + Nueva carpeta + Abrir con + Buscar + Detalles + Enviar + Configuraciones + Ordenar + Usuario activo + Aún no hay actividades + Enviar + Enviar liga a… + Actividad + Agregar a %1$s + Permitir volver a compartir + La cuenta aún no ha sido agregada a este dispositivo + Ya existe una cuenta en el dispositivo para el mismo usuario y servidor + El usuario ingresado no corresponde con el usuario de esta cuenta + Versión del servidor no reconocida + Conexión establecida + Dirección del servidor https://… + El formato de dirección para el servidor es erróneo + El servidor no se encontró + No hay conexión de red + Conexión segura no disponible. + La configuración del servidor está mal formada. + Autorización no exitosa + Acceso denegado por el servidor de autorización + La conexión segura se está redirigiendo a través de una ruta insegura. + Conexión segura establecida + Falla en la inicialización de SSL + No fue posible verificar la identidad del servidor SSL + Probando conexión + El servidor tardó demasiado en responder + Intentando iniciar sesión… + Nombre de usuario o contraseña incorrecto + Error desconocido: %1$s + ¡Se presentó un error HTTP desconocido! + ¡Se presentó un error desconocido! + No fue posible encontrar el servidor + %1$s no soporta cuentas múltiples + No fue posible establecer la conexión + Solo cargar sobre una Wi-Fi no tarificada + /CargaAutomática + Crear nueva configuración de carpeta personalizada + Configurar una carpeta personalizada + Avatar + Cerrar + Deshabilitar + Calendario + Se presentó un problema al cargar el certificado. + Casilla de verificación + Elige la carpeta local… + Elige la carpeta remota… + Texto copiado desde %1$s + No se ha recibido texto para copiar al portapapeles + Se presentó un error inesperado al copiar al portapapeles + Atrás + Cancelar + Cancelar sincronización + Selecciona una cuenta + Confirmar + Copiar + Borrar + Error + Memoria insuficiente + Error desconocido + Cargando + Siguiente + No + OK + Pendiente + Eliminar + Renombrar + Guardar + Enviar + Compartir + Omitir + Sí + Probar la versión de desarrollo + Esto incluye todas las últimas funcionalidades y es lo más nuevo. Fallas/errores pueden ocurrir y si es el caso, por favor repórtanoslo. + foro + Ayuda a otros en + Contribuye activamente + la aplicación + Traducir + Obtén la liberación de desarrollo directamente + Obtén la liberación de desarrollo de la tienda de aplicaciones F-Droid + Obtén el candidato a liberación de la tienda de aplicaciones F-Droid + Obtén el candidato a liberación de la tienda de Google Play + Candidato a lanzamiento + El candidato a lanzamiento (RC) es una foto del lanzamiento más próximo y se espera que sea estable. Hacer preubas en tu configuración individual puede ayudarnos a asegurar ésto. Anótate para probar en la Play store o consulta manualmente la sección de \"Versión\" de F-Droid. + ¿Encontraste una falla? ¿Hay algo raro? + Ayúdanos probando + Reporta un tema en GitHub + ¿Realmente deseas elminiar %1$s? + ¿Realmente deseas borrar los elementos seleccionados? + ¿Realmente quieres eliminar %1$s y sus contenidos? + ¿Reamente deseas eliminar los elementos seleccionados y sus contenidos? + Sólo local + Ícono de usuario para la lista de contactos + No se han otorgado privilegios, nada fue importado. + Contactos + Respaldar ahora + El respaldo está calendarizado y deberá iniciar en breve + La importación está calendarizada y deberá iniciar en breve + No se encontraron archivos + ¡No fue posible encontrar tu último respaldo! + Copiado al portapapeles + Se presentó un error al intentar copiar este archivo o carpeta + No es posible copiar una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta destino + Copiar liga + Copiar/ mover a la carpeta encriptada no se encuentra soportado por el momento. + Crear + No fue posible crear la carpeta + Nuevo + Carpeta nueva + Cerrar sesión + Deseleccionar todo + Nueva versión disponible + No hay información disponible. + No hay una nueva versión disponible. + Cerrar + Este algoritmo de verificación no está disponible en tu teléfono + Deshabilitar + Descartar + Terminado + No fue posible descargar %1$s + La descarga falló, inicia sesión de nuevo + Falla en la descarga + El archivo ya no se encuentra disponible en el servidor + %1$d%% Descargando %2$s + Descargando… + %1$s descargado + Descargado + Aún no ha descargado + Imagen de fondo para el encabezado de la barra de navegación + Actividades + Todos los archivos + Favoritos + Inicio + Notificaciones + Disponibles sin conexión + Modificado recientemente + Compartido + Papelera + Cargas + Cerrar sesión + %1$s de %2$s usados + Carga automática + Hablar + Establecer como encriptado + Configurar la encripción + Desencriptando… + Cerrar + Esta carpeta no está vacía. + Generando nuevas llaves… + Las 12 palabras juntas hacer una contraseña muy fuerte, permitiéndote solo a ti ver y hacer uso de tus archivos encriptados. Por favor escríbelo y mantenlo en un lugar seguro. + La encripción de punta-a-punta está dehabilitada en el servidor. + Anota tu contraseña de encripción de 12 palabras + Contraseña… + Recuperando llaves… + Almacenando llaves + Configurar encripción + No se pudieron guardar las llaves, por favor vuelve a intentarlo. + Se presentó un error al desencriptar. ¿Contraseña equivocada? + %1$s no pudo ser copiado a la carpeta local %2$s + Error crítico: No se pueden realizar operaciones + Cuentas + Creado + Usuario + Trabajos en segundo plano + Descargar + Cargar + Agregar a tus favoritos + Hacer favorito + Borrar + Archivo + Carga algún contenido o sincroniza con tus dispositivos. + Aún no hay nada marcado como favorito + No hay archivos aquí + No hay resultados en esta carpeta + No hay resultados + No hay nada aquí. Puedes agregar una carpeta. + No se encontraron archivos modificados dentro de los últimos 7 días + ¿Tal vez está dentro de una carpeta diferente? + Aún no hay nada compartido + carpeta + Cargando… + No se cuenta con una aplicación que maneje este tipo de archivo. + hace algunos segundos + Verificando el destino… + Limpiando… + La carpeta de datos ya existe. Selecciona una de las siguientes: + La carpeta Nextcloud ya existe + Se necesita más espacio + No fue posible leer el archivo fuente + No fue posible escribir al archivo destino + Se presentó una falla durante la migración + Se presentó una falla al actualizar el índice + Moviendo los datos… + Terminado + Reemplazar + Preparando la migración… + Restaurando la configuración de la cuenta… + Guardando la configuración de la cuenta… + ¡No se puede leer la carpeta fuente! + Actualizando índice… + Usar + Aguardando sincronización completa… + Archivo no encontrado + El archivo no pudo ser sincronizado. Mostrando la última versión disponible. + Renombrar + Detalles + Descargar + Exportar + El archivo se renombró com %1$s durante la carga + Sincronizar + No se ha seleccionado algún archivo + El nombre del archivo no puede estar en blanco. + Caracteres inválidos: / \\ < > : \" | ? * + El nombre del archivo contiene al menos un carácter inválido + Nombre del archivo + Crear + No hay carpetas aquí + Seleccionar + Mover + No se te permite %s + para copiar este archivo + para crear este archivo + para borrar este archivo + para mover este archivo + para renombrar este archivo + Cargando archivos… + Algunos archivos no se pudieron mover + Local: %1$s + Mover todos + Remoto: %1$s + Se han movido todos los archivos + Adelante + Nombre + Contraseña + Cargar archivos sólo con el dispositivo conectado a la toma de corriente + /Cargas Automáticas + Invisible + Liga + Vista de lista + No hay archivos en esta carpeta + No se encontró el archivo en el sistema de archivos local + No hay más carpetas. +  %1$s bitácora de aplicación Android + Iniciar sesión + Actualizar + Cargando... + Bitácoras + Borrar datos + Las configuraciones, base de datos y certificados del servidor de los datos de %1$s serán borrados permanentemente.\n\nLos archivos descargados se mantendrán sin cambios.\n\nEste proceso puede tomar algo de tiempo. + Administrar espacio + No fue posible leer el archivo de medios + El archivo de medios tiene una codificación incorrecta + El intento de reproducir el archivo sobrepasó el tiempo de espera + El reproductor de medios integrado es incapaz de reproducir el archivo de medios + Codec no soportado + Botón de avanzar rápido + Reproductor de música %1$s + Botón de reproducir o pausar + Botón de rebobinar + %1$s (reproduciendo) + Más reciente primero + Más antiguo primero + A - Z + Z - A + Más grande primero + Más pequeño primero + Más + Se presentó un error al intentar mover este archivo o carpeta + No es posible mover una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta de destino + Se presentó un error durante la conexión al servidor + foto + Muestra el progreso de la descarga + Descargas + Muestra el progreso de la sincronización de archivos y los resultados + Sincronizar archivo + Progreso del reproductor de música + Reproductor de medios + Mostrar las notificaciones push enviadas por el servidor: Menciones en comentarios, recepción de nuevos elementos compartidos remotos, anuncios publicados por el administrador, etc. + Notificaciones push + Muestra el progreso de la carga + Cargas + No hay notificaciones + Por favor verifica más tarde. + 1 hora + Ingresa tu código de seguridad + El código de seguridad será solicitado cada vez que inicie la aplicación + Por favor ingresa tu código de seguridad + Los códigos de seguridad no son iguales + Por favor reingresa el código de seguridad + Elimina tu código de seguirdad + Código de seguirdad eliminado + Código de seguridad almacenado + Código de seguridad incorrecto + Se requieren permisos adicionales para cargar y descargar archivos. + No se encontró una aplicación con la cual establecer la imagen + 389 KB + marcadordeposición.txt + 12:23:45 + Este es un marcador de posición + 2012/05/18 12:23 PM + Borrado + mantenido en la carpeta original + movido a la carpeta de la aplicación + Agregar cuenta + No ha sido instalado ni F-Droid o Google Play + Acerca de + Detalles + Desarrollo + General + Más + Sincronizar + Respaldo diario de tus contactos + GNU Licencia Pública General, versión 2 + Ayuda + Exención de responsabilidad + El archivo original será… + El archivo original será… + Usar sub carpetas + Licencia + Ninguno + Administrar cuentas + Mostrar archivos ocultos + Obtener el código fuente + Carpeta local + Carpeta remota + Tema + Oscuro + Claro + Vista previa de imagen + No existe un archivo local a previsualizar + No es posible mostrar la imagen + Disculpa + Privacidad + Las notificaciones push están deshabilitadas debido a dependencias de servicios propietarios de Google Play. + No hay notificaciones push debido a un inicio de sesión caduco. Por favor vuelve a ingresar a tu cuenta. + En este momento las notificaciones push no están disponibles. + ¡Prueba %1$s en tu dispositivo! + Quiero invitarte a usar %1$s en tu dispositivo\nDescárgalo aquí:%2$s + %1$s ó %2$s + Volver a cargar + Falla al eliminar + Eliminar + Eliminado + Ingresa un nombre nuevo + No se pudo renombrar la copia local, intenta con un nombre diferente + No fue posible renombrar, el nombre ya está ocupado + No está permitido recompartir + Reintentar + Seleccionar todo + Enviar + Ícono de botón de envío + Establecer Como + Usar imagen como + Compartir + Compartiendo + Compartir %1$s + %1$s (grupo) + Compartir liga + Debes ingresar una contraseña + para compartir este archivo + Ingresa una contraseña + Establecer fecha de expiración + Establecer contraseña + Puede editar + %1$s (remoto) + Configuraciones + Compartir liga + Compartir con… + compartido + Ordenar por + Ocultar + Detalles + La identidad del servidor no pudo ser verificada + País: + Nombre común: + Ubicación: + Organización: + Unidad organizacional: + Estado: + Huella digital: + Emitido por: + Firma: + Algoritmo: + Emitido para: + Validez: + De: + Para: + - No hay información acerca del error + No fue posible guardar el certificado + El certificado no puede ser mostrado. + ¿Quieres confiar en este certificado de todas formas? + - El certificado del servidor expiró + - El certificado del servidor no es de confianza + - Las fechas del certificado del servidor están en el futuro + - La URL no corresponde con el nombre del servidor en el certificado + Mensaje de estado + Predeterminado + Descargas + \"%1$s\" ha sido compartido contigo + %1$s ha compartido \"%2$s\" contigo + Sincronizar + Se encontraron conflictos + La carpeta %1$s ya no existe + No fue posible sincronizar %1$s + Contraseña equivocada para %1$s + Falla en archivos mantenidos-en-sincronización. + Falla en la sincronización + La sincronización falló, inicia sesión de nuevo + Los contenidos del archivo ya han sido sincronizados + A partir de la versión 1.3.16, los archivos cargados desde este dispositivo serán copiados a la carpeta local %1$s para prevenir pérdidas de datos cuando un archivo se sincroniza entre múltiples cuentas. \n\nDerivado de este cambio, todos los archivos cargados con versiones anteriores de la aplicación fueron copiados a la carpeta %2$s. Sin embargo, un error evitó que se completara esta operación durante la sincronización de la cuenta. Puedes dejar el(los) archivo(s) como está(n) y eliminar la liga a %3$s, o bien, mover el(los) archivo(s) a la carpeta %1$s y mantener la liga a %4$s.\n\nSe enlistan a continuación los archivos locales así como los archivos remotos en %5$s a donde estaban ligados + Algunos archivos locales se han perdido + Obteniendo la versión más reciente del archivo. + Botón de status de sincronización + Archivos + Botón de configuración + Configurar carpetas + Las cargas instantáneas se han mejorado por completo. Re-configura tu carga automática desde el menú principal.\n\nDisfruta la nueva y mejorada carga automática. + Para %1$s + Tipo + Etiquetas + Probar la conexión del servidor + 30 minutos + Esta semana + Miniatura + Hoy + Papelera + No hay archivos borrados + Borrar permanentemente + Desestablecer encripción + para dejar de compartir este archivo + Accede a través de un dominio no de confianza. Por favor consulta la documentación para más información. + para actualizar este recurso compartido + Borrar cargas fallidas + Reintentar cargas fallidas + Cargar forma… + Cargar contenido de otras aplicaciones + Nombre de archivo + Tipo de archivo + Archivo de acceso directo a Google Maps(%s) + Archivo de acceso directo a Internet(%s) + Archivo snippet de texto(.txt) + Ingresa el nombre y el tipo del archivo a cargar + Cargar archivos + Botón de cargar elemento + Borrar + No hay cargas disponibles + Carga algún contenido o activa la carga automática + El espacio insuficiente evita que se copien los archivos seleccionados dentro de la carpeta %1$s. ¿Te gustaría moverlos ahí en su lugar? + Error desconocido + Seleccionar + Cargar + La información recibida no contiene un archivo válido. + %1$s no tiene permitido leer un archivo recibido + No fue posible copiar el archivo a una carpeta temporal. Por favor intenta enviarlo de nuevo. + El archivo seleccionado para cargar no fue encontrado. Por favor verifica si el archivo existe. + No hay un archivo a cargar + Nombre de la carpeta + Selecciona la carpeta de cargas + No fue posible cargar %1$s + La carga falló, inicia sesión de nuevo + Falla en la carga + Opción de carga: + Mover el archivo a la carpeta %1$s + Mantener el archivo en la carpeta de origen + Borrar el archivo de la carpeta de origen + para cargar a esta carpeta + %1$d%% Cargando %2$s + Cargando + %1$s cargado + Salir + Ajustes + No existen cuentas %1$s en tu dispositivo. Por favor configura una cuenta primero. + No se encontró la cuenta + Actual + Fallido/pendiente de reinicio + Cargado + Cancelado + Esperando para cargar + Cargas + Cancelado + Conflicto + Error de conexión + Error de credenciales + Error de archivo + Error de carpeta + No se encontró el archivo local + Error de permisos + El certificado del servidor no es de confianza + La aplicación ha sido terminada + Completado + Error desconocido + Virus detectado. ¡La carga no puede ser completada! + Esperando a salir de modo de conservación de energía + Aguardando la recarga del dispositivo + Usuario + Dirección + Correo electrónico + Número telefónico + Twitter + Sitio web + Error al extraer la información del usuario + No se ha establecido la información personal + Agrega tu nombre, una imagen y detalles de contacto en tu página de perfil. + Usuario + Descargar + Aguarda un momento… + Verificando credenciales almacenadas + Copiando el archivo desde almacenamiento privado + Actualizar + Imagen de qué es nuevo + Omitir + Nuevo en %1$s + Descargando archivos… + Enviar correo electrónico + diff --git a/app/src/main/res/values-es-rGT/strings.xml b/app/src/main/res/values-es-rGT/strings.xml new file mode 100644 index 000000000000..0af19f46813d --- /dev/null +++ b/app/src/main/res/values-es-rGT/strings.xml @@ -0,0 +1,536 @@ + + + %1$s aplicación Android + Acerca de + versión %1$s + Ícono de la cuenta + ¡No se encontró la cuenta! + Editar + Enviar/Compartir + Vista de cuadrícula + Vista de lista + Nueva carpeta + Abrir con + Buscar + Detalles + Enviar + Configuraciones + Ordenar + Usuario activo + Aún no hay actividades + Enviar + Enviar liga a… + Actividad + Agregar a %1$s + Permitir volver a compartir + La cuenta aún no ha sido agregada a este dispositivo + Ya existe una cuenta en el dispositivo para el mismo usuario y servidor + El usuario ingresado no corresponde con el usuario de esta cuenta + Versión del servidor no reconocida + Conexión establecida + Dirección del servidor https://… + El formato de dirección para el servidor es erróneo + El servidor no se encontró + No hay conexión de red + Conexión segura no disponible. + La configuración del servidor está mal formada. + Autorización no exitosa + Acceso denegado por el servidor de autorización + La conexión segura se está redirigiendo a través de una ruta insegura. + Conexión segura establecida + Falla en la inicialización de SSL + No fue posible verificar la identidad del servidor SSL + Probando conexión + El servidor tardó demasiado en responder + Intentando iniciar sesión… + Nombre de usuario o contraseña incorrecto + Error desconocido: %1$s + ¡Se presentó un error HTTP desconocido! + ¡Se presentó un error desconocido! + No fue posible encontrar el servidor + %1$s no soporta cuentas múltiples + No fue posible establecer la conexión + Solo cargar sobre una Wi-Fi no tarificada + /CargaAutomática + Crear nueva configuración de carpeta personalizada + Configurar una carpeta personalizada + Avatar + Cerrar + Deshabilitar + Calendario + Se presentó un problema al cargar el certificado. + Casilla de verificación + Elige la carpeta local… + Elige la carpeta remota… + Texto copiado desde %1$s + No se ha recibido texto para copiar al portapapeles + Se presentó un error inesperado al copiar al portapapeles + Atrás + Cancelar + Cancelar sincronización + Selecciona una cuenta + Confirmar + Copiar + Borrar + Error + Memoria insuficiente + Error desconocido + Cargando + Siguiente + No + OK + Pendiente + Eliminar + Renombrar + Guardar + Enviar + Compartir + Omitir + Sí + Probar la versión de desarrollo + Esto incluye todas las últimas funcionalidades y es lo más nuevo. Fallas/errores pueden ocurrir y si es el caso, por favor repórtanoslo. + foro + Ayuda a otros en + Contribuye activamente + la aplicación + Traducir + Obten la liberación de desarrollo directamente + Obten la liberación de desarrollo de la tienda de aplicaciones F-Droid + Obten el candidato a liberación de la tienda de aplicaciones F-Droid + Obten el candidato a liberación de la tienda de Google Play + Candidato a lanzamiento + El candidato a lanzamiento (RC) es una foto del lanzamiento más próximo y se espera que sea estable. Hacer preubas en tu configuración individual puede ayudarnos a asegurar ésto. Anótate para probar en la Play store o consulta manualmente la sección de \"Versión\" de F-Droid. + ¿Encontraste una falla? ¿Hay algo raro? + Ayúdanos probando + Reporta un tema en GitHub + ¿Realmente deseas elminiar %1$s? + ¿Realmente deseas borrar los elementos seleccionados? + ¿Realmente quieres eliminar %1$s y sus contenidos? + ¿Reamente deseas eliminar los elementos seleccionados y sus contenidos? + Sólo local + Ícono de usuario para la lista de contactos + No se han otorgado privilegios, nada fue importado. + Contactos + Respaldar ahora + El respaldo está calendarizado y deberá iniciar en breve + La importación está calendarizada y deberá iniciar en breve + No se encontraron archivos + ¡No fue posible encontrar tu último respaldo! + Copiado al portapapeles + Se presentó un error al intentar copiar este archivo o carpeta + No es posible copiar una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta destino + Copiar liga + Copiar/ mover a la carpeta encriptada no se encuentra soportado por el momento. + Crear + No fue posible crear la carpeta + Nuevo + Carpeta nueva  + Cerrar sesión + Deseleccionar todo + Nueva versión disponible + No hay información disponible. + No hay una nueva versión disponible. + Cerrar + Este algoritmo de verificación no está disponible en tu teléfono + Deshabilitar + Descartar + Terminado + No fue posible descargar %1$s + La descarga falló, inicia sesión de nuevo + Falla en la descarga + El archivo ya no se encuentra disponible en el servidor + %1$d%% Descargando %2$s + Descargando… + %1$s descargado + Descargado + Aún no ha descargado + Imagen de fondo para el encabezado de la barra de navegación + Actividades + Todos los archivos + Favoritos + Inicio + Notificaciones + Disponibles sin conexión + Modificado recientemente + Compartido + Papelera + Cargas + Cerrar sesión + %1$s de %2$s usados + Carga automática + Hablar + Establecer como encriptado + Configurar la encripción + Decriptando… + Cerrar + Esta carpeta no está vacía. + Generando nuevas llaves… + Las 12 palabras juntas hacer una contraseña muy fuerte, permitiendote solo a ti ver y hacer uso de tus archivos encriptados. Por favor escríbelo y mantenlo en un lugar seguro. + La encripción de punta-a-punta está dehabilitada en el servidor. + Anota tu contraseña de encripción de 12 palabras + Contraseña… + Recuperando llaves… + Almacenando llaves + Configurar encripción + No se pudieron guardar las llaves, por favor vuelve a intentarlo. + Se presentó un error al decriptar. ¿Contraseña equivocada? + %1$s no pudo ser copiado a la carpeta local %2$s + Error crítico: No se pueden realizar operaciones + Cuentas + Creado + Ususario + Trabajos en segundo plano + Descargar + Cargar + Agregar a tus favoritos + Hacer favorito + Borrar + Archivo + Carga algún contenido o sincroniza con tus dispositivos. + Aún no hay nada marcado como favorito + No hay archivos aquí + No hay resultados en esta carpeta + No hay resultados + No hay nada aquí. Puedes agregar una carpeta. + No se encontraron archivos modificados dentro de los últimos 7 días + ¿Tal vez está dentro de una carpeta diferente? + Aún no hay nada compartido + carpeta + Cargando… + No se cuenta con una aplicación que maneje este tipo de achivo. + hace algunos segundos + Verificando el destino… + Limpiando… + La carpeta de datos ya existe. Selecciona una de las siguientes: + La carpeta Nextcloud ya existe + Se necesita más espacio + No fue posible leer el archivo fuente + No fue posible escribir al archivo destino + Se presentó una falla durante la migración + Se presentó una falla al actualizar el índice + Moviendo los datos… + Terminado + Reemplazar + Preparando la migración… + Restaurando la configuración de la cuenta… + Guardando la configuración de la cuenta… + ¡No se puede leer la carpeta fuente! + Actualizando índice… + Usar + Agurardando sincronización completa… + Archivo no encontrado + El archivo no pudo ser sincronizado. Mostrando la última versión disponible. + Renombrar + Detalles + Descargar + Exportar + El archivo se renombró com %1$s durante la carga + Syncronizar + No se ha seleccionado algún archivo + El nombre del archivo no puede estar en blanco. + Caracteres inválidos: / \\ < > : \" | ? * + El nombre del archivo contiene al menos un carácter inválido + Nombre del archivo + Crear + No hay carpetas aquí + Seleccionar + Mover + No se te permite %s + para copiar este archivo + para crear este archivo + para borrar este archivo + para mover este archivo + para renombrar este archivo + Cargando archivos… + Algunos archivos no se pudieron mover + Local: %1$s + Mover todos + Remoto: %1$s + Se han movido todos los archivos + Adelante + Nombre + Contraseña + Cargar archivos sólo con el dispositivo conectado a la toma de corriente + /Cargas Automáticas + Invisible + Liga + Vista de lista + No hay archivos en esta carpeta + No se encontró el archivo en el sistema de archivos local + No hay más carpetas. +  %1$s bitácora de aplicación Android + Iniciar sesión + Actualizar + Cargando... + Bitácoras + Borrar datos + Las configuraciones, base de datos y certificados del servior de los datos de %1$s serán borrados permanentemente.\n\nLos archivos descargados se mantendrán sin cambios.\n\nEste proceso puede tomar algo de tiempo. + Administrar espacio + No fue posible leer el archivo de medios + El archivo de medios tiene una codificación incorrecta + El intento de reproducir el archivo sobrepasó el tiempo de espera + El reproductor de medios integrado es incapaz de reproducir el archivo de medios + Codec no soportado + Botón de avanzar rápido + Reproductor de músca %1$s + Botón de reproducir o pausar + Botón de rebobinar + %1$s (reproduciendo) + Más reciente primero + Más antiguo primero + A - Z + Z - A + Más grande primero + Más pequeño primero + Más + Se presentó un error al intentar mover este archivo o carpeta + No es posible mover una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta de destino + Se presentó un error durante la conexión al servidor + foto + Muestra el progreso de la descarga + Descargas + Muestra el progreso de la sincronización de archivos y los resultados + Sincronizar archivo + Progreso del reproductor de música + Reproductor de medios + Mostrar las notificaciones push enviadas por el servidor: Menciones en comentarios, recepción de nuevos elementos compartidos remotos, anuncios publicados por el administrador, etc. + Notificaciones push + Muestra el progreso de la carga + Cargas + No hay notificaciones + Por favor verifica más tarde. + 1 hora + Ingresa tu código de seguridad + El código de seguridad será solicitado cada vez que inicie la aplicación + Por favor ingresa tu código de seguridad + Los códigos de seguridad no son iguales + Por favor reingresa el código de seguridad + Elimina tu código de seguirdad + Código de seguirdad eliminado + Código de seguridad almacenado + Código de seguridad incorrecto + Se requieren permisos adicionales para cargar y descargar archivos. + No se encontró una aplicación con la cual establecer la imagen + 389 KB + marcadordeposición.txt + 12:23:45 + Este es un marcador de posición + 2012/05/18 12:23 PM + Borrado + mantenido en la carpeta original + movido a la capreta de la aplicación + Agregar cuenta + No ha sido instalado ni F-Droid o Google Play + Acerca de + Detalles + Desarrollo + General + Más + Syncronizar + Respaldo diario de tus contactos + GNU Licencia Pública General, versión 2 + Ayuda + Excención de responsabilidad + El archivo original será… + El archivo original será… + Usar sub carpetas + Licencia + Ninguno + Administrar cuentas + Mostrar archivos ocultos + Obtener el código fuente + Carpeta local + Carpeta remota + Tema + Oscuro + Claro + Vista previa de imagen + No existe un archivo local a previsualizar + No es posible mostrar la imagen + Disculpa + Privacidad + Las notificaciones push están deshabilitadas debido a dependencias de servicios propietarios de Google Play. + No hay notificaciones push debido a un incio de sesión caduco. Por favor vuelve a ingresar a tu cuenta. + En este momento las notificaciones push no están disponibles. + ¡Prueba %1$s en tu dispositivo! + Quiero invitarte a usar %1$s en tu dispositivo\nDescargalo aquí:%2$s + %1$s ó %2$s + Volver a cargar + Falla al eliminar + Eliminar + Eliminado + Ingresa un nombre nuevo + No se pudo renombrar la copia local, intenta con un nombre diferente + No fue posible renombrar, el nombre ya esta ocupado + No está permitido recompartir + Reintentar + Seleccionar todo + Enviar + Ícono de botón de envío + Establecer Como + Usar imagen como + Compartir + Compartiendo + Compartir %1$s + %1$s (grupo) + Compartir liga + Debes ingresar una contraseña + para compartir este archivo + Ingresa una contraseña + Establecer fecha de expiración + Establecer contraseña + Puede editar + %1$s (remoto) + Configuraciones + Compartir liga + Compartir con… + compartido + Ordenar por + Ocultar + Detalles + La identidad del servidor no pudo ser verificada + País: + Nombre común: + Ubicación: + Organización: + Unidad organizacional: + Estado: + Huella digital: + Emitido por: + Firma: + Algoritmo: + Emitido para: + Validez: + De: + Para: + - No hay información acerca del error + No fue posible guardar el certificado + El certificado no puede ser mostrado. + ¿Quieres confiar en este certificado de todas formas? + - El certificado del servidor expiró + - El certificado del servidor no es de confianza + - Las fechas del certificado del servidor están en el futuro + - La URL no corresponde con el nombre del servidor en el certificado + Mensaje de estado + Predeterminado + Descargas + \"%1$s\" ha sido compartido contigo + %1$s ha compartido \"%2$s\" contigo + Syncronizar + Se encontraron conflictos + La carpeta %1$s ya no existe + No fue posible sincronizar %1$s + Contraseña equivocada para %1$s + Falla en archivos mantenidos-en-sincronización. + Falla en la sincronización + La sincronización falló, inicia sesión de nuevo + Los contenidos del archivo ya han sido sincronizados + A partir de la versión 1.3.16, los archivos cargados desde este dispositivo serán copiados a la carpeta local %1$spara prevenir perdidas de datos cuando un archivo se sincroniza entre múltiples cuentas. \n\nDerivado de este cambio, todos los archivos cargados con versiones anteriores de la aplicación fueron copiados a la carpeta %2$s. Sin embargo, un error evitó que se completara esta operación durante la sincronizacion de la cuenta. Puedes dejar el(los) archivo(s) como está(n) y eliminar la liga a %3$s, o bien, mover el(los) archivo(s) a la carpeta%1$s y mantener la liga a %4$s.\n\nSe enlistan a continuación los archivos locales así como los archivos remotos en %5$s a donde estaban ligados + Algunos archivos locales se han perdido + Obteniendo la versión más reciente del archivo. + Botón de status de sincronización + Archivos + Botón de configuración + Configurar carpetas + Las cargas instantaneas se han mejorado por completo. Re-configura tu carga automática desde el menú principal.\n\nDisfruta la nueva y mejorada carga automática. + Para %1$s + Tipo + Etiquetas + Probar la conexión del servidor + 30 minutos + Esta semana + Miniatura + Hoy + Papelera + No hay archivos borrados + Borrar permanentemente + Desestablecer encripción + para dejar de compartir este archivo + Accede a través de un dominio no de confianza. Por favor consulta la documentación para más información. + para actualizar este recurso compartido + Borrar cargas fallidas + Reintentar cargas fallidas + Cargar forma… + Cargar contenido de otras aplicaciones + Foto + Nombre de archivo + Tipo de archivo + Archivo de acceso directo a Google Maps(%s) + Archivo de acceso directo a Internet(%s) + Archivo snippet de texto(.txt) + Ingresa el nombre y el tipo del archivo a cargar + Cargar archivos + Botón de cargar elemento + Borrar + No hay cargas disponibles + Carga algún contenido o activa la carga automática + El espacio insuficiente evita que se copien los archivos seleccionados dentro de la carpeta %1$s. ¿Te gustaría moverlos ahí en su lugar? + Error desconocido + Seleccionar + Cargar + La información recibida no contiene un archivo válido. + %1$s no tiene permitido leer un archivo recibido + No fue posible copiar el archivo a una carpeta temporal. Por favor intenta enviarlo de nuevo. + El archivo seleccionado para cargar no fue encontrado. Por favor verifica si el archivo existe. + No hay un archivo a cargar + Nombre de la carpeta + Selecciona la carpeta de cargas + No fue posible cargar %1$s + La carga falló, inicia sesión de nuevo + Falla en la carga + Opción de carga: + Mover el archivo a la carpeta %1$s + Mantener el archivo en la carpeta de origen + Borrar el archivo de la carpeta de origen + para cargar a esta carpeta + %1$d%% Cargando %2$s + Cargando + %1$s cargado + Salir + Ajustes + No existen cuentas %1$s en tu dispositivo. Por favor configura una cuenta primero. + No se encontró la cuenta + Actual + Fallido/pendiente de reinicio + Cargado + Cancelado + Esperando para cargar + Cargas + Cancelado + Conflicto + Error de conexión + Error de credenciales + Error de archivo + Error de carpeta + No se encontró el archivo local + Error de permisos + El certificado del servidor no es de confianza + La aplicación ha sido terminada + Completado + Error desconocido + Virus detectado. ¡La carga no puede ser completada! + Esperando a salir de modo de conservación de energía + Aguardando la recarga del dispositivo + Ususario + Dirección + Correo electrónico + Número telefónico + Twitter + Sitio web + Error al extraer la información del usuario + No se ha establecido la información personal + Agrega tu nombre, una imagen y detalles de contacto en tu página de perfil. + Usuario + Descargar + Agurarda un momento… + Verificando credenciales almacenadas + Copiando el archivo desde almacenamiento privado + Actualizar + Imagen de qué es nuevo + Omitir + Nuevo en %1$s + Descargando archivos… + Enviar correo electrónico + diff --git a/app/src/main/res/values-es-rSV/strings.xml b/app/src/main/res/values-es-rSV/strings.xml new file mode 100644 index 000000000000..fac4570b78e5 --- /dev/null +++ b/app/src/main/res/values-es-rSV/strings.xml @@ -0,0 +1,536 @@ + + + %1$s aplicación Android + Acerca de + versión %1$s + Ícono de la cuenta + ¡No se encontró la cuenta! + Editar + Enviar/Compartir + Vista de cuadrícula + Vista de lista + Nueva carpeta + Abrir con + Buscar + Detalles + Enviar + Configuraciones + Ordenar + Usuario activo + Aún no hay actividades + Enviar + Enviar liga a… + Actividad + Agregar a %1$s + Permitir volver a compartir + La cuenta aún no ha sido agregada a este dispositivo + Ya existe una cuenta en el dispositivo para el mismo usuario y servidor + El usuario ingresado no corresponde con el usuario de esta cuenta + Versión del servidor no reconocida + Conexión establecida + Dirección del servidor https://… + El formato de dirección para el servidor es erróneo + El servidor no se encontró + No hay conexión de red + Conexión segura no disponible. + La configuración del servidor está mal formada. + Autorización no exitosa + Acceso denegado por el servidor de autorización + La conexión segura se está redirigiendo a través de una ruta insegura. + Conexión segura establecida + Falla en la inicialización de SSL + No fue posible verificar la identidad del servidor SSL + Probando conexión + El servidor tardó demasiado en responder + Intentando iniciar sesión… + Nombre de usuario o contraseña incorrecto + Error desconocido: %1$s + ¡Se presentó un error HTTP desconocido! + ¡Se presentó un error desconocido! + No fue posible encontrar el servidor + %1$s no soporta cuentas múltiples + No fue posible establecer la conexión + Solo cargar sobre una Wi-Fi no tarificada + /CargaAutomática + Crear nueva configuración de carpeta personalizada + Configurar una carpeta personalizada + Avatar + Cerrar + Deshabilitar + Calendario + Se presentó un problema al cargar el certificado. + Casilla de verificación + Elige la carpeta local… + Elige la carpeta remota… + Texto copiado desde %1$s + No se ha recibido texto para copiar al portapapeles + Se presentó un error inesperado al copiar al portapapeles + Atrás + Cancelar + Cancelar sincronización + Selecciona una cuenta + Confirmar + Copiar + Borrar + Error + Memoria insuficiente + Error desconocido + Cargando + Siguiente + No + OK + Pendiente + Eliminar + Renombrar + Guardar + Enviar + Compartir + Omitir + Sí + Probar la versión de desarrollo + Esto incluye todas las últimas funcionalidades y es lo más nuevo. Fallas/errores pueden ocurrir y si es el caso, por favor repórtanoslo. + foro + Ayuda a otros en + Contribuye activamente + la aplicación + Traducir + Obtén la liberación de desarrollo directamente + Obtén la liberación de desarrollo de la tienda de aplicaciones F-Droid + Obtén el candidato a liberación de la tienda de aplicaciones F-Droid + Obtén el candidato a liberación de la tienda de Google Play + Candidato a lanzamiento + El candidato a lanzamiento (RC) es una foto del lanzamiento más próximo y se espera que sea estable. Hacer preubas en tu configuración individual puede ayudarnos a asegurar ésto. Anótate para probar en la Play store o consulta manualmente la sección de \"Versión\" de F-Droid. + ¿Encontraste una falla? ¿Hay algo raro? + Ayúdanos probando + Reporta un tema en GitHub + ¿Realmente deseas elminiar %1$s? + ¿Realmente deseas borrar los elementos seleccionados? + ¿Realmente quieres eliminar %1$s y sus contenidos? + ¿Reamente deseas eliminar los elementos seleccionados y sus contenidos? + Sólo local + Ícono de usuario para la lista de contactos + No se han otorgado privilegios, nada fue importado. + Contactos + Respaldar ahora + El respaldo está calendarizado y deberá iniciar en breve + La importación está calendarizada y deberá iniciar en breve + No se encontraron archivos + ¡No fue posible encontrar tu último respaldo! + Copiado al portapapeles + Se presentó un error al intentar copiar este archivo o carpeta + No es posible copiar una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta destino + Copiar liga + Copiar/ mover a la carpeta encriptada no se encuentra soportado por el momento. + Crear + No fue posible crear la carpeta + Nuevo + Carpeta nueva  + Cerrar sesión + Deseleccionar todo + Nueva versión disponible + No hay información disponible. + No hay una nueva versión disponible. + Cerrar + Este algoritmo de verificación no está disponible en tu teléfono + Deshabilitar + Descartar + Terminado + No fue posible descargar %1$s + La descarga falló, inicia sesión de nuevo + Falla en la descarga + El archivo ya no se encuentra disponible en el servidor + %1$d%% Descargando %2$s + Descargando… + %1$s descargado + Descargado + Aún no ha descargado + Imagen de fondo para el encabezado de la barra de navegación + Actividades + Todos los archivos + Favoritos + Inicio + Notificaciones + Disponibles sin conexión + Modificado recientemente + Compartido + Papelera + Cargas + Cerrar sesión + %1$s de %2$s usados + Carga automática + Hablar + Establecer como encriptado + Configurar la encripción + Desencriptando… + Cerrar + Esta carpeta no está vacía. + Generando nuevas llaves… + Las 12 palabras juntas hacer una contraseña muy fuerte, permitiéndote solo a ti ver y hacer uso de tus archivos encriptados. Por favor escríbelo y mantenlo en un lugar seguro. + La encripción de punta-a-punta está dehabilitada en el servidor. + Anota tu contraseña de encripción de 12 palabras + Contraseña… + Recuperando llaves… + Almacenando llaves + Configurar encripción + No se pudieron guardar las llaves, por favor vuelve a intentarlo. + Se presentó un error al desencriptar. ¿Contraseña equivocada? + %1$s no pudo ser copiado a la carpeta local %2$s + Error crítico: No se pueden realizar operaciones + Cuentas + Creado + Usuario + Trabajos en segundo plano + Descargar + Cargar + Agregar a tus favoritos + Hacer favorito + Borrar + Archivo + Carga algún contenido o sincroniza con tus dispositivos. + Aún no hay nada marcado como favorito + No hay archivos aquí + No hay resultados en esta carpeta + No hay resultados + No hay nada aquí. Puedes agregar una carpeta. + No se encontraron archivos modificados dentro de los últimos 7 días + ¿Tal vez está dentro de una carpeta diferente? + Aún no hay nada compartido + carpeta + Cargando… + No se cuenta con una aplicación que maneje este tipo de archivo. + hace algunos segundos + Verificando el destino… + Limpiando… + La carpeta de datos ya existe. Selecciona una de las siguientes: + La carpeta Nextcloud ya existe + Se necesita más espacio + No fue posible leer el archivo fuente + No fue posible escribir al archivo destino + Se presentó una falla durante la migración + Se presentó una falla al actualizar el índice + Moviendo los datos… + Terminado + Reemplazar + Preparando la migración… + Restaurando la configuración de la cuenta… + Guardando la configuración de la cuenta… + ¡No se puede leer la carpeta fuente! + Actualizando índice… + Usar + Aguardando sincronización completa… + Archivo no encontrado + El archivo no pudo ser sincronizado. Mostrando la última versión disponible. + Renombrar + Detalles + Descargar + Exportar + El archivo se renombró com %1$s durante la carga + Sincronizar + No se ha seleccionado algún archivo + El nombre del archivo no puede estar en blanco. + Caracteres inválidos: / \\ < > : \" | ? * + El nombre del archivo contiene al menos un carácter inválido + Nombre del archivo + Crear + No hay carpetas aquí + Seleccionar + Mover + No se te permite %s + para copiar este archivo + para crear este archivo + para borrar este archivo + para mover este archivo + para renombrar este archivo + Cargando archivos… + Algunos archivos no se pudieron mover + Local: %1$s + Mover todos + Remoto: %1$s + Se han movido todos los archivos + Adelante + Nombre + Contraseña + Cargar archivos sólo con el dispositivo conectado a la toma de corriente + /Cargas Automáticas + Invisible + Liga + Vista de lista + No hay archivos en esta carpeta + No se encontró el archivo en el sistema de archivos local + No hay más carpetas. +  %1$s bitácora de aplicación Android + Iniciar sesión + Actualizar + Cargando... + Bitácoras + Borrar datos + Las configuraciones, base de datos y certificados del servidor de los datos de %1$s serán borrados permanentemente.\n\nLos archivos descargados se mantendrán sin cambios.\n\nEste proceso puede tomar algo de tiempo. + Administrar espacio + No fue posible leer el archivo de medios + El archivo de medios tiene una codificación incorrecta + El intento de reproducir el archivo sobrepasó el tiempo de espera + El reproductor de medios integrado es incapaz de reproducir el archivo de medios + Codec no soportado + Botón de avanzar rápido + Reproductor de música %1$s + Botón de reproducir o pausar + Botón de rebobinar + %1$s (reproduciendo) + Más reciente primero + Más antiguo primero + A - Z + Z - A + Más grande primero + Más pequeño primero + Más + Se presentó un error al intentar mover este archivo o carpeta + No es posible mover una carpeta dentro de una de sus sub carpetas + El archivo ya existe en la carpeta de destino + Se presentó un error durante la conexión al servidor + foto + Muestra el progreso de la descarga + Descargas + Muestra el progreso de la sincronización de archivos y los resultados + Sincronizar archivo + Progreso del reproductor de música + Reproductor de medios + Mostrar las notificaciones push enviadas por el servidor: Menciones en comentarios, recepción de nuevos elementos compartidos remotos, anuncios publicados por el administrador, etc. + Notificaciones push + Muestra el progreso de la carga + Cargas + No hay notificaciones + Por favor verifica más tarde. + 1 hora + Ingresa tu código de seguridad + El código de seguridad será solicitado cada vez que inicie la aplicación + Por favor ingresa tu código de seguridad + Los códigos de seguridad no son iguales + Por favor reingresa el código de seguridad + Elimina tu código de seguirdad + Código de seguirdad eliminado + Código de seguridad almacenado + Código de seguridad incorrecto + Se requieren permisos adicionales para cargar y descargar archivos. + No se encontró una aplicación con la cual establecer la imagen + 389 KB + marcadordeposición.txt + 12:23:45 + Este es un marcador de posición + 2012/05/18 12:23 PM + Borrado + mantenido en la carpeta original + movido a la carpeta de la aplicación + Agregar cuenta + No ha sido instalado ni F-Droid o Google Play + Acerca de + Detalles + Desarrollo + General + Más + Sincronizar + Respaldo diario de tus contactos + GNU Licencia Pública General, versión 2 + Ayuda + Exención de responsabilidad + El archivo original será… + El archivo original será… + Usar sub carpetas + Licencia + Ninguno + Administrar cuentas + Mostrar archivos ocultos + Obtener el código fuente + Carpeta local + Carpeta remota + Tema + Oscuro + Claro + Vista previa de imagen + No existe un archivo local a previsualizar + No es posible mostrar la imagen + Disculpa + Privacidad + Las notificaciones push están deshabilitadas debido a dependencias de servicios propietarios de Google Play. + No hay notificaciones push debido a un inicio de sesión caduco. Por favor vuelve a ingresar a tu cuenta. + En este momento las notificaciones push no están disponibles. + ¡Prueba %1$s en tu dispositivo! + Quiero invitarte a usar %1$s en tu dispositivo\nDescárgalo aquí:%2$s + %1$s ó %2$s + Volver a cargar + Falla al eliminar + Eliminar + Eliminado + Ingresa un nombre nuevo + No se pudo renombrar la copia local, intenta con un nombre diferente + No fue posible renombrar, el nombre ya está ocupado + No está permitido recompartir + Reintentar + Seleccionar todo + Enviar + Ícono de botón de envío + Establecer Como + Usar imagen como + Compartir + Compartiendo + Compartir %1$s + %1$s (grupo) + Compartir liga + Debes ingresar una contraseña + para compartir este archivo + Ingresa una contraseña + Establecer fecha de expiración + Establecer contraseña + Puede editar + %1$s (remoto) + Configuraciones + Compartir liga + Compartir con… + compartido + Ordenar por + Ocultar + Detalles + La identidad del servidor no pudo ser verificada + País: + Nombre común: + Ubicación: + Organización: + Unidad organizacional: + Estado: + Huella digital: + Emitido por: + Firma: + Algoritmo: + Emitido para: + Validez: + De: + Para: + - No hay información acerca del error + No fue posible guardar el certificado + El certificado no puede ser mostrado. + ¿Quieres confiar en este certificado de todas formas? + - El certificado del servidor expiró + - El certificado del servidor no es de confianza + - Las fechas del certificado del servidor están en el futuro + - La URL no corresponde con el nombre del servidor en el certificado + Mensaje de estado + Predeterminado + Descargas + \"%1$s\" ha sido compartido contigo + %1$s ha compartido \"%2$s\" contigo + Sincronizar + Se encontraron conflictos + La carpeta %1$s ya no existe + No fue posible sincronizar %1$s + Contraseña equivocada para %1$s + Falla en archivos mantenidos-en-sincronización. + Falla en la sincronización + La sincronización falló, inicia sesión de nuevo + Los contenidos del archivo ya han sido sincronizados + A partir de la versión 1.3.16, los archivos cargados desde este dispositivo serán copiados a la carpeta local %1$s para prevenir pérdidas de datos cuando un archivo se sincroniza entre múltiples cuentas. \n\nDerivado de este cambio, todos los archivos cargados con versiones anteriores de la aplicación fueron copiados a la carpeta %2$s. Sin embargo, un error evitó que se completara esta operación durante la sincronización de la cuenta. Puedes dejar el(los) archivo(s) como está(n) y eliminar la liga a %3$s, o bien, mover el(los) archivo(s) a la carpeta %1$s y mantener la liga a %4$s.\n\nSe enlistan a continuación los archivos locales así como los archivos remotos en %5$s a donde estaban ligados + Algunos archivos locales se han perdido + Obteniendo la versión más reciente del archivo. + Botón de status de sincronización + Archivos + Botón de configuración + Configurar carpetas + Las cargas instantáneas se han mejorado por completo. Re-configura tu carga automática desde el menú principal.\n\nDisfruta la nueva y mejorada carga automática. + Para %1$s + Tipo + Etiquetas + Probar la conexión del servidor + 30 minutos + Esta semana + Miniatura + Hoy + Papelera + No hay archivos borrados + Borrar permanentemente + Desestablecer encripción + para dejar de compartir este archivo + Accede a través de un dominio no de confianza. Por favor consulta la documentación para más información. + para actualizar este recurso compartido + Borrar cargas fallidas + Reintentar cargas fallidas + Cargar forma… + Cargar contenido de otras aplicaciones + Foto + Nombre de archivo + Tipo de archivo + Archivo de acceso directo a Google Maps(%s) + Archivo de acceso directo a Internet(%s) + Archivo snippet de texto(.txt) + Ingresa el nombre y el tipo del archivo a cargar + Cargar archivos + Botón de cargar elemento + Borrar + No hay cargas disponibles + Carga algún contenido o activa la carga automática + El espacio insuficiente evita que se copien los archivos seleccionados dentro de la carpeta %1$s. ¿Te gustaría moverlos ahí en su lugar? + Error desconocido + Seleccionar + Cargar + La información recibida no contiene un archivo válido. + %1$s no tiene permitido leer un archivo recibido + No fue posible copiar el archivo a una carpeta temporal. Por favor intenta enviarlo de nuevo. + El archivo seleccionado para cargar no fue encontrado. Por favor verifica si el archivo existe. + No hay un archivo a cargar + Nombre de la carpeta + Selecciona la carpeta de cargas + No fue posible cargar %1$s + La carga falló, inicia sesión de nuevo + Falla en la carga + Opción de carga: + Mover el archivo a la carpeta %1$s + Mantener el archivo en la carpeta de origen + Borrar el archivo de la carpeta de origen + para cargar a esta carpeta + %1$d%% Cargando %2$s + Cargando + %1$s cargado + Salir + Ajustes + No existen cuentas %1$s en tu dispositivo. Por favor configura una cuenta primero. + No se encontró la cuenta + Actual + Fallido/pendiente de reinicio + Cargado + Cancelado + Esperando para cargar + Cargas + Cancelado + Conflicto + Error de conexión + Error de credenciales + Error de archivo + Error de carpeta + No se encontró el archivo local + Error de permisos + El certificado del servidor no es de confianza + La aplicación ha sido terminada + Completado + Error desconocido + Virus detectado. ¡La carga no puede ser completada! + Esperando a salir de modo de conservación de energía + Aguardando la recarga del dispositivo + Usuario + Dirección + Correo electrónico + Número telefónico + Twitter + Sitio web + Error al extraer la información del usuario + No se ha establecido la información personal + Agrega tu nombre, una imagen y detalles de contacto en tu página de perfil. + Usuario + Descargar + Aguarda un momento… + Verificando credenciales almacenadas + Copiando el archivo desde almacenamiento privado + Actualizar + Imagen de qué es nuevo + Omitir + Nuevo en %1$s + Descargando archivos… + Enviar correo electrónico + diff --git a/app/src/main/res/values-es/ionos-strings.xml b/app/src/main/res/values-es/ionos-strings.xml new file mode 100644 index 000000000000..2adc1986df08 --- /dev/null +++ b/app/src/main/res/values-es/ionos-strings.xml @@ -0,0 +1,35 @@ + + + + + Debe iniciar sesión a través del navegador + Bienvenido al almacenamiento en la nube + Conexión + Cancelar + Repetir + Aceptar + Esta aplicación utiliza cookies y tecnologías similares. Al hacer clic en Aceptar, consientes el procesamiento y la transferencia de tus datos a terceros. Encontrarás más información, incluida la relativa al procesamiento de datos por parte de terceros, en los ajustes y en nuestra política de privacidad Puedes rechazar la utilización de estas herramientas reject o modificar tu selección en cualquier momento a través de tus ajustes. + Estos datos nos ayudan a optimizar el uso de la aplicación y a identificar los fallos del sistema y los errores más rápidamente. + Análisis de la recopilación de datos para un diseño personalizado basado en las necesidades de cada usuario + Ajustes + La recopilación de estos datos es necesaria para poder utilizar las funciones esenciales de la aplicación. + Recopilación necesaria de datos + Para optimizar nuestra aplicación, recogemos datos anónimos. Para ello utilizamos soluciones de software de diferentes socios. Nos gustaría ofrecerte una transparencia y una ayuda en la toma de decisiones total sobre el procesamiento y la recopilación de tus datos de uso anonimizados. Posteriormente, puedes cambiar tu configuración en cualquier momento en el punto de menú Protección de datos. Sin embargo, ten en cuenta que la recopilación de datos contribuye de manera significativa a la optimización de esta aplicación y que si detienes el envío de datos impedirás estas optimizaciones. + Guardar ajustes + Configuración de la protección de datos + Permisos de almacenamiento + Permisos de almacenamiento + %1$s necesita permiso para acceder a sus archivos para una mejor funcionalidad. Puede permitir o denegar el acceso. + %1$s necesita permiso para acceder a sus archivos para una mejor funcionalidad. Puede permitir o denegar el acceso. + Nombre o dirección de correo electrónico... + Almacenamiento utilizado + Filtro + IONOS HiDrive Next necesita permiso para acceder a la cámara + AJUSTES + Intentando iniciar sesión… + diff --git a/app/src/main/res/values-fr/ionos-strings.xml b/app/src/main/res/values-fr/ionos-strings.xml new file mode 100644 index 000000000000..9e46b63f1a6d --- /dev/null +++ b/app/src/main/res/values-fr/ionos-strings.xml @@ -0,0 +1,35 @@ + + + + + Un navigateur s\'ouvre pour le login + Bienvenue sur votre cloud + Connexion + Annuler + Répéter + Accepter + Cette application utilise des cookies et des technologies similaires. En cliquant sur Accepter, vous acceptez le traitement et le transfert de vos données à des tiers. Vous trouverez plus d’informations, notamment sur le traitement des données par des tiers, dans les paramètres et dans notre Politique de confidentialité Vous pouvez refuser d\'utiliser les outils Refuser ou modifier votre sélection à tout moment via vos paramètres. + Ces données nous aident à optimiser l\'utilisation de l\'application et à identifier plus rapidement les pannes du système et les erreurs. + Analyse de la collecte de données pour une conception basée sur les besoins + Paramétrages + La collecte de ces données est nécessaire afin d\'utiliser les fonctions essentielles de l\'application. + Collecte de données nécessaire + Pour optimiser notre application, nous recueillons des données anonymes. Pour ce faire, nous utilisons les solutions logicielles de différents partenaires. Nous souhaitons vous offrir une transparence et un pouvoir de décision entiers sur le traitement et la collecte de vos données d\'utilisation anonymes. Vous pouvez également modifier vos paramètres à tout moment par la suite dans le menu Protection des données. Veuillez toutefois noter que la collecte de données contribue de manière significative à l\'optimisation de cette application et que vous empêchez ces optimisations en arrêtant le transfert de données. + Sauvegarder les paramètres + Paramètres de confidentialité + Autorisations de stockage + Autorisations de stockage + %1$s a besoin d\'une autorisation pour accéder à vos fichiers. Vous pouvez autoriser ou refuser l\'accès. + %1$s a besoin d\'une autorisation pour accéder à vos fichiers. Vous pouvez autoriser ou refuser l\'accès. + Nom ou adresse e-mail… + Stockage occupé + Filtre + IONOS HiDrive Next a besoin d\'une autorisation pour accéder à la caméra + PARAMÈTRES + Tentative de connexion… + diff --git a/app/src/main/res/values-it/ionos-strings.xml b/app/src/main/res/values-it/ionos-strings.xml new file mode 100644 index 000000000000..83ef64b034bd --- /dev/null +++ b/app/src/main/res/values-it/ionos-strings.xml @@ -0,0 +1,36 @@ + + + + + È necessario effettuare il login tramite il browser + Benvenuti nel cloud storage + Accedi + Annulla + Riprova + Consenti + Questa applicazione utilizza cookie e tecnologie simili. Facendo clic su Accetto, si acconsente al trattamento dei propri dati e al loro trasferimento a terzi. Visitate la nostra Privacy Policy per ulteriori informazioni, anche sul trattamento dei dati da parte di fornitori terzi. Cliccate su Rifiuta per impedire l\'uso dello strumento, oppure modificate la vostra scelta in qualsiasi momento nelle Impostazioni. + Questi dati ci aiutano a ottimizzare l\'esperienza dell\'utente e a identificare crash e bug del sistema. + Analisi dei dati per una progettazione basata sulla usabilità + Impostazioni + La raccolta dei dati è necessaria per utilizzare le funzioni chiave dell\'applicazione. + Raccolta dei dati necessari + Raccogliamo dati anonimizzati per ottimizzare la nostra applicazione. A tale scopo utilizziamo soluzioni software di vari partner. Il nostro intento è quello di offrire la massima trasparenza e di consentire all\'utente di decidere come vengono raccolti e utilizzati i suoi dati anonimizzati. È possibile modificare le impostazioni in qualsiasi momento anche in seguito, alla voce di menu Privacy. Si prega di notare, tuttavia, che la raccolta dei dati contribuisce in modo significativo all\'ottimizzazione di questa applicazione e che l\'utente impedirebbe questi miglioramenti rifiutando il trasferimento dei dati. + Salva le impostazioni + Impostazioni sulla privacy + Autorizzazioni di archiviazione + Autorizzazioni di archiviazione + %1$s ha bisogno del permesso di accedere ai file per una migliore funzionalità. È possibile consentire o negare l\'accesso. + + %1$s ha bisogno del permesso di accedere ai file per una migliore funzionalità. È possibile consentire o negare l\'accesso. + Nome o indirizzo e-mail... + Storage usato + Filtro + IONOS HiDrive Next ha bisogno di un\'autorizzazione per accedere alla telecamera + IMPOSTAZIONI + Tentativo di accesso in corso… + diff --git a/app/src/main/res/values-land/ionos-integers.xml b/app/src/main/res/values-land/ionos-integers.xml new file mode 100644 index 000000000000..69325648f8cb --- /dev/null +++ b/app/src/main/res/values-land/ionos-integers.xml @@ -0,0 +1,11 @@ + + + + + 4 + diff --git a/app/src/main/res/values-land/player-dimens.xml b/app/src/main/res/values-land/player-dimens.xml new file mode 100644 index 000000000000..b10f63164684 --- /dev/null +++ b/app/src/main/res/values-land/player-dimens.xml @@ -0,0 +1,18 @@ + + + + 56dp + + 8dp + 0dp + 8dp + + 0dp + + 8dp + \ No newline at end of file diff --git a/app/src/main/res/values-large-land/ionos-integers.xml b/app/src/main/res/values-large-land/ionos-integers.xml new file mode 100644 index 000000000000..69325648f8cb --- /dev/null +++ b/app/src/main/res/values-large-land/ionos-integers.xml @@ -0,0 +1,11 @@ + + + + + 4 + diff --git a/app/src/main/res/values-large-land/player-dimens.xml b/app/src/main/res/values-large-land/player-dimens.xml new file mode 100644 index 000000000000..992ff0ac03a9 --- /dev/null +++ b/app/src/main/res/values-large-land/player-dimens.xml @@ -0,0 +1,18 @@ + + + + 56dp + + 24dp + 8dp + 24dp + + 16dp + + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values-large/ionos-dimens.xml b/app/src/main/res/values-large/ionos-dimens.xml new file mode 100644 index 000000000000..364b222d48a8 --- /dev/null +++ b/app/src/main/res/values-large/ionos-dimens.xml @@ -0,0 +1,29 @@ + + + + + + + 36dp + + + 48dp + 128dp + 35sp + 49sp + 32dp + 18sp + 24sp + 24dp + 24dp + 480dp + 16dp + 288dp + 12dp + + \ No newline at end of file diff --git a/app/src/main/res/values-large/ionos-integers.xml b/app/src/main/res/values-large/ionos-integers.xml new file mode 100644 index 000000000000..48b14899437c --- /dev/null +++ b/app/src/main/res/values-large/ionos-integers.xml @@ -0,0 +1,11 @@ + + + + + 3 + diff --git a/app/src/main/res/values-large/player-dimens.xml b/app/src/main/res/values-large/player-dimens.xml new file mode 100644 index 000000000000..339a72a3e6c2 --- /dev/null +++ b/app/src/main/res/values-large/player-dimens.xml @@ -0,0 +1,21 @@ + + + + 64dp + 12dp + + 24dp + 24dp + 48dp + + 48dp + + 40dp + + 72dp + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 72908c65f15b..7142d58da573 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -16,7 +16,7 @@ #ffffff - #121212 + @color/refresh_black #000000 #818181 #222222 diff --git a/app/src/main/res/values-night/ionos-colors.xml b/app/src/main/res/values-night/ionos-colors.xml new file mode 100644 index 000000000000..77b35e3a90c4 --- /dev/null +++ b/app/src/main/res/values-night/ionos-colors.xml @@ -0,0 +1,49 @@ + + + @color/bg_default + + @color/white + @color/primary + @color/primary_dark + @color/disabled_text + + @color/primary + @color/white + + @color/curious_blue + + @color/primary + + @color/dodger_blue + @color/royal_blue + @color/dark_slate_grey + + @color/white + @color/white + @color/dusk + + @color/transparent + @color/ghost_white + @color/transparent + @color/ghost_white + @color/ghost_white + @color/dusk + @color/ghost_white + @color/madison + @color/dusk + + @color/curious_blue + @color/cornflower + @color/dusk + + @color/dark_slate_grey + @color/metallic_silver + @color/dusk + @color/deep_koamaru + @color/midnight_blue2 + diff --git a/app/src/main/res/values-night/ionos-scanbot-theme.xml b/app/src/main/res/values-night/ionos-scanbot-theme.xml new file mode 100644 index 000000000000..709c3f4e2d95 --- /dev/null +++ b/app/src/main/res/values-night/ionos-scanbot-theme.xml @@ -0,0 +1,47 @@ + + + + + + diff --git a/app/src/main/res/values-nl/ionos-strings.xml b/app/src/main/res/values-nl/ionos-strings.xml new file mode 100644 index 000000000000..4712e9412dee --- /dev/null +++ b/app/src/main/res/values-nl/ionos-strings.xml @@ -0,0 +1,35 @@ + + + + + U moet inloggen via de browser + Welkom bij cloud-opslag + Aanmelden + Afbreken + Herhaling + Akkoord + Deze toepassing gebruikt cookies en vergelijkbare technologie. Door akkoord te gaan, accepteer je de verwerking en de overdracht van je gegevens aan derden. privacybeleid . Je kunt het gebruik van de tools op elk moment weigeren of je keuze aanpassen via je instellingen. + Deze gegevens helpen ons om het gebruik van de app voor je te optimaliseren en om systeemcrashes en fouten sneller te identificeren. + Dataverzameling voor gebruikersgebaseerde app-optimalisatie + Instellingen + Het verzamelen van deze gegevens is noodzakelijk om essentiële functies van de app te kunnen gebruiken. + Noodzakelijke dataverzameling + Om onze app te optimaliseren, verzamelen we anonieme gegevens. Hiervoor gebruiken we softwareoplossingen van verschillende partners. Wij willen je volledige transparantie en beslissingsbevoegdheid geven over de verwerking en verzameling van je geanonimiseerde gebruiksgegevens. Je kunt je instellingen altijd wijzigen via het menu \'Privacy\'. Houd er rekening mee dat het verzamelen van gegevens een aanzienlijke bijdrage levert aan de optimalisatie van deze app. Als je de dataverzameling uitschakelt, zijn deze optimalisaties beperkt of niet meer mogelijk. + Instellingen opslaan + Privacyinstellingen + Opslagmachtigingen + Opslagmachtigingen + %1$s heeft toestemming nodig om toegang te krijgen tot je bestanden voor betere functionaliteit. Je kunt toegang toestaan of weigeren. + %1$s heeft toestemming nodig om toegang te krijgen tot je bestanden voor betere functionaliteit. Je kunt toegang toestaan of weigeren. + Naam of e-mailadres... + Gebruikte opslag + Filter + IONOS HiDrive Next heeft toestemming nodig voor toegang tot camera + INSTELLINGEN + Proberen om in the loggen… + diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 7c949e01fbb8..4a5cf55c72b8 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -41,6 +41,7 @@ 4dp 300dp 14dp + 10dp 12sp 20dp diff --git a/app/src/main/res/values/ionos-attrs.xml b/app/src/main/res/values/ionos-attrs.xml new file mode 100644 index 000000000000..009589e48d7f --- /dev/null +++ b/app/src/main/res/values/ionos-attrs.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ionos-bools.xml b/app/src/main/res/values/ionos-bools.xml new file mode 100644 index 000000000000..e6feeb2dc73e --- /dev/null +++ b/app/src/main/res/values/ionos-bools.xml @@ -0,0 +1,11 @@ + + + + + false + diff --git a/app/src/main/res/values/ionos-colors-palette.xml b/app/src/main/res/values/ionos-colors-palette.xml new file mode 100644 index 000000000000..a4800777a5d5 --- /dev/null +++ b/app/src/main/res/values/ionos-colors-palette.xml @@ -0,0 +1,40 @@ + + + + + #0B2A63 + #003d8f + #02102B + #001B41 + #095BB1 + #465A75 + #DBEDF8 + #718095 + #1D2D42 + #F4F7FA + #33F4F7FA + #1474C4 + #C36B00 + #FFAA00 + #2E4360 + #97A3B4 + #DBE2E8 + #F2F5F8 + #95CAEB + #BCC8D4 + #0C8A44 + #BDCDD7 + #3196D6 + #586680 + #FF6159 + #80000000 + #2102102B + #D9FFFFFF + #D902102B + #121212 + \ No newline at end of file diff --git a/app/src/main/res/values/ionos-colors.xml b/app/src/main/res/values/ionos-colors.xml new file mode 100644 index 000000000000..0eed63de3f4c --- /dev/null +++ b/app/src/main/res/values/ionos-colors.xml @@ -0,0 +1,77 @@ + + + #3672be + #d62e2b + + @color/bg_default + + @color/white + @color/primary + @color/primary_dark + @color/disabled_text + + @color/primary + @color/black + + + @color/ghost_white + @color/dodger_blue + + @color/dodger_blue + + @color/bg_default + ?android:textColorPrimary + + @color/primary + @color/background_color_inverse + @color/color_transparent + @color/black + @color/black + @color/grey_900 + + @color/text_color + @color/primary + @color/primary + @color/disabled_text + @color/primary + @color/primary + + @color/primary + @color/primary + @color/white + + @color/bg_default + + @color/madison + @color/dodger_blue + @color/gull_grayapprox + + @color/white + @color/white + @color/geyser + + @color/transparent + @color/navy + @color/transparent + @color/madison + @color/navy + @color/gull_grayapprox + @color/madison + @color/white + @color/gull_grayapprox + + @color/dodger_blue + @color/royal_blue + @color/gull_grayapprox + + @color/ghost_white + @color/dusk + @color/metallic_silver + @color/geyser + @color/ghost_white + diff --git a/app/src/main/res/values/ionos-dimens.xml b/app/src/main/res/values/ionos-dimens.xml new file mode 100644 index 000000000000..4ae79784a1ad --- /dev/null +++ b/app/src/main/res/values/ionos-dimens.xml @@ -0,0 +1,37 @@ + + + + + + + 8dp + + + 16dp + 64dp + 18sp + 24sp + 32dp + 14sp + 19sp + 12dp + 24dp + 288dp + 16dp + 288dp + 12dp + + 1dp + 8dp + 14sp + 16dp + + 16dp + 2dp + 5dp + \ No newline at end of file diff --git a/app/src/main/res/values/ionos-integers.xml b/app/src/main/res/values/ionos-integers.xml new file mode 100644 index 000000000000..4529d1a3c362 --- /dev/null +++ b/app/src/main/res/values/ionos-integers.xml @@ -0,0 +1,11 @@ + + + + + 2 + diff --git a/app/src/main/res/values/ionos-scanbot-theme.xml b/app/src/main/res/values/ionos-scanbot-theme.xml new file mode 100644 index 000000000000..3865b99fbe96 --- /dev/null +++ b/app/src/main/res/values/ionos-scanbot-theme.xml @@ -0,0 +1,47 @@ + + + + + + diff --git a/app/src/main/res/values/ionos-strings.xml b/app/src/main/res/values/ionos-strings.xml new file mode 100644 index 000000000000..34c40cfb456b --- /dev/null +++ b/app/src/main/res/values/ionos-strings.xml @@ -0,0 +1,35 @@ + + + + + You need to login over browser + Welcome to the cloud storage + Log In + Cancel + Retry + Agree + This application uses cookies and similar technologies. By clicking on Agree, you consent to the processing of your data and also to its transfer to third parties. Visit our Privacy Policy for more information, also on data processing by third party providers. Click on Reject to prevent use of the tool, or change your selection at any time in Settings. + This data helps us to optimize your user experience and to identify system crashes and bugs. + Data analysis for needs-based design + Settings + Data collection is necessary in order to use key functions of the app. + Necessary data collection + We collect anonymized data to optimize our app. We use software solutions by various partners for this purpose. Our intention is to offer full transparency and allow you to decide how your anonymized data is collected and used. You can also change your settings at any time later on under the Privacy menu item. Please note, however, that data collection contributes significantly to optimizing this app and that you would prevent these improvements by rejecting the transfer of data. + Save Settings + Privacy Settings + Storage permissions + Storage permissions + %1$s needs permission to access your files for better functionality. You can allow or deny access. + %1$s needs permission to access your files for better functionality. You can allow or deny access. + Name or email address… + Storage used + Filter + IONOS HiDrive Next needs permission to access camera + SETTINGS + Trying to log in… + diff --git a/app/src/main/res/values/ionos-styles.xml b/app/src/main/res/values/ionos-styles.xml new file mode 100644 index 000000000000..463a95f26579 --- /dev/null +++ b/app/src/main/res/values/ionos-styles.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/ionos-themes.xml b/app/src/main/res/values/ionos-themes.xml new file mode 100644 index 000000000000..4c28d61a9f96 --- /dev/null +++ b/app/src/main/res/values/ionos-themes.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4bc5a363883..ac6a19dce6a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,11 @@ Favorites Assistant Media + + Update Android System WebView + Please update the Android System WebView app for a login + Update + Strict mode: no HTTP connection allowed! Poor connection Check your internet connection or try again later %1$s Android app @@ -25,6 +30,7 @@ Details Send Sort + Action triggered Sort by A - Z Z - A @@ -59,6 +65,7 @@ Assistant + Loading task list… The task output is not ready yet. Failed to send a message Failed to fetch chat messages @@ -68,13 +75,14 @@ Task list is empty. Check assistant app configuration. Delete task Are you sure you want to delete this task? + Add new task + Delete Task Task created An error occurred while creating the task Task deleted An error occurred while deleting the task Open conversation list - Go back to assistant page Send message Input Output @@ -375,6 +383,10 @@ Exported %d file, skipped rest due to error Exported %d files, skipped rest due to error + + %d file will be exported. See notification for details. + %d files will be exported. See notification for details. + You can upload only %d file at once. You can upload up to %d files at once. @@ -511,6 +523,7 @@ 2012/05/18 12:23 PM 12:23:45 This is a placeholder + placeholder.txt Filename Recently edited .txt @@ -583,6 +596,7 @@ File is currently locked by another user or process and therefore not deletable. Please try again later. Sorry + File does not exist File is not downloaded Downloading image to start the edit screen, please wait… Image preview @@ -749,6 +763,7 @@ Share link Send link Set password + Share with… Unset Name, Federated Cloud ID or email address… @@ -767,6 +782,7 @@ Settings, database and server certificates from %1$s\'s data will be deleted permanently. \n\nDownloaded files will be kept untouched.\n\nThis process can take a while. Clear data + Additional permissions required to upload and download files. File not found in local file system Do you really want to delete the selected items? Do you really want to delete the selected items and their contents? @@ -910,7 +926,9 @@ shared More menu Type - + Allow resharing + Go back to assistant page + Sync status button Sync warning button Settings button @@ -1036,6 +1054,7 @@ New version was created New comment… Error commenting file + Successfully restored file version. Error restoring file version! General notifications Show notifications for new media folders and similar @@ -1141,6 +1160,7 @@ Shared with you by %1$s Retrieving file… Associated account not found! + Logged in as %1$s Error retrieving file Could not retrieve URL Failed to pass file to download manager @@ -1184,6 +1204,7 @@ Report issue to tracker? (requires a GitHub account) Report %1$s crashed + Copied to clipboard Download latest dev version Changelog dev version Could not download full image @@ -1229,6 +1250,7 @@ QR code could not be read! Note icon Add new public share link + New name Share link (%1$s) Share link Custom permissions @@ -1239,6 +1261,7 @@ Allow download and sync Create Edit + View only Share Delete Editing @@ -1246,9 +1269,13 @@ Failed to update UI Failed to pick email address. (remote) + Set status Online status Status message What is your status? + Clear status message after + Clear status message + Set status message Clear status after Online Do not disturb @@ -1301,7 +1328,10 @@ Create Please select one template Please choose a template and enter a file name. + File drop Share permissions + Advanced settings + Next Send share Please select at least one permission to share. @@ -1493,6 +1523,7 @@ Sync duplication Could not load content The device is likely not connected to the internet + Link not followed due to security settings. Unknown Upload Stopped – Storage Permission Required Your files cannot be uploaded without access to local storage. Tap to grant permission. @@ -1505,7 +1536,6 @@ Can’t create items here: missing create permission. Can’t create file: missing permission. Can’t create folder: missing permission. - Link not followed due to security settings. You don’t have permission to create or upload files in this folder. Busy Mute all notifications @@ -1522,7 +1552,6 @@ Cannot open file chooser Send copy to Failed to start action! - Action triggered File upload conflicts Upload conflicts detected. Open uploads to resolve. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3b9d1ef0ecb9..b2d7961cdb0b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,7 +11,7 @@ ~ SPDX-FileCopyrightText: 2012 Bartosz Przybylski ~ SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) --> - + - @@ -117,7 +125,7 @@ true - @@ -220,7 +228,7 @@ @color/black @style/Theme.ownCloud.Overlay.ActionBar @style/ToolbarStyle.Overflow - @style/ThemeOverlay.App.BottomSheetDialog + @style/Generic.ThemeOverlay.App.BottomSheetDialog + diff --git a/scanbot/src/main/res/values-nl/strings.xml b/scanbot/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000000..6eefb8bc06d4 --- /dev/null +++ b/scanbot/src/main/res/values-nl/strings.xml @@ -0,0 +1,58 @@ + + + + + Automatisch + Geen document + Slecht licht + Te onrustige achtergrond + Niet bewegen + Perspectief + Blijf dichter bij het document + Flash + Importeren + Afbreken + Fout bij het aanmaken van het document + Document detecteren + scan_%1$tY%1$tm%1$td-%1$tH%1$tM%1$tS + Wijzigingen gaan verloren + Document scannen + Er is een fout opgetreden. Probeer het later nog eens. + Kan bestand niet importeren %s + Filter toepassen op alle beelden + Zwart & Wit + Kleur + Grijswaarden + Magic Color + Magic Text + Geen filter + Solliciteer voor alle + Uitvallen + Verwijderen + Filter + Herschikken + Draaien + %d van %d + Vasthouden en slepen om pagina\'s te herschikken + Camera-toestemming nodig om code te scannen + Er is niet genoeg opslagruimte beschikbaar op het apparaat om foto\'s te kunnen maken. Maak meer ruimte vrij. + Niet genoeg ruimte + De smartphone heeft niet genoeg opslagruimte. + OK + Document verwerken ... + Verwerken... + Pagina\'s herschikken + Kader opnieuw instellen + Opslaan + Behalve als + Bestandsnaam: + Bestandstype: + Ongeldige bestandsnaam + Opslaglocatie: + Er is een fout opgetreden. Probeer het later nog eens. + diff --git a/scanbot/src/main/res/values/attrs.xml b/scanbot/src/main/res/values/attrs.xml new file mode 100644 index 000000000000..078446388b23 --- /dev/null +++ b/scanbot/src/main/res/values/attrs.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scanbot/src/main/res/values/colors-palette.xml b/scanbot/src/main/res/values/colors-palette.xml new file mode 100644 index 000000000000..43b859132093 --- /dev/null +++ b/scanbot/src/main/res/values/colors-palette.xml @@ -0,0 +1,26 @@ + + + + + #FFFFFF + #000000 + #333333 + #999999 + #dcdcdc + #EBEEEF + #222222 + #00FFFFFF + #0D000000 + #55FF8800 + #ccFF8800 + #a0c8e8 + #D9555555 + #8C0077BB + #0077BB + #003d8f + \ No newline at end of file diff --git a/scanbot/src/main/res/values/dimens.xml b/scanbot/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..447b7f44876c --- /dev/null +++ b/scanbot/src/main/res/values/dimens.xml @@ -0,0 +1,58 @@ + + + + + 4dp + 8dp + 12dp + 16dp + 24dp + + 18sp + + 1dp + + 56dp + 48dp + + 2dp + 48dp + + 48dp + 10dp + 16dp + 36dp + + 64dp + + 20dp + 20dp + 30dp + + 4dp + 4dp + 24dp + 8dp + 11sp + 5dp + 16dp + 40dp + + 50dp + 250dp + 15dp + 20sp + 20dp + 10dp + 75dp + 18sp + + 480dp + + 20sp + \ No newline at end of file diff --git a/scanbot/src/main/res/values/preference_keys.xml b/scanbot/src/main/res/values/preference_keys.xml new file mode 100644 index 000000000000..def3f6a319f8 --- /dev/null +++ b/scanbot/src/main/res/values/preference_keys.xml @@ -0,0 +1,14 @@ + + + + + preference_scanbot_license_key + + preference_aes_gsm_key + preference_aes_gsm_initialization_vector + \ No newline at end of file diff --git a/scanbot/src/main/res/values/strings.xml b/scanbot/src/main/res/values/strings.xml new file mode 100644 index 000000000000..a0d5478008c0 --- /dev/null +++ b/scanbot/src/main/res/values/strings.xml @@ -0,0 +1,58 @@ + + + + + Automatic + No Document + Poor light + Background too noisy + Don\'t move + Perspective + Move closer + Flash + Import + Cancel + Error while creating document + Detect document + scan_%1$tY%1$tm%1$td-%1$tH%1$tM%1$tS + Progress will be lost + Scan document + An error occurred. Please try again later. + Can\'t import file %s + Applying filter for all images + Black & White + Color + Grayscale + Magic Color + Magic Text + No filter + Apply for all + Crop + Delete + Filter + Rearrange + Rotate + %d of %d + Hold and drag to rearrange pages + Need use camera permissions to be granted to scan code + Not enough space on device for taking photo. Please, clear storage. + Not enough space + The smartphone does not have enough storage space available. + OK + Processing document… + Processing… + Rearrange pages + Reset borders + Save + Save as + File name: + File type: + Invalid file name + Save location: + An error occurred. Please try again later + diff --git a/scanbot/src/main/res/values/styles.xml b/scanbot/src/main/res/values/styles.xml new file mode 100644 index 000000000000..a0f2eb4ff932 --- /dev/null +++ b/scanbot/src/main/res/values/styles.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/scanbot/src/main/res/values/theme.xml b/scanbot/src/main/res/values/theme.xml new file mode 100644 index 000000000000..fee2f03abf28 --- /dev/null +++ b/scanbot/src/main/res/values/theme.xml @@ -0,0 +1,71 @@ + + + + + diff --git a/scanbot/src/main/res/values/untranslatable_strings.xml b/scanbot/src/main/res/values/untranslatable_strings.xml new file mode 100644 index 000000000000..7d520ab5d97c --- /dev/null +++ b/scanbot/src/main/res/values/untranslatable_strings.xml @@ -0,0 +1,14 @@ + + + + + JPG + PDF + PDF (OCR) + PNG + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 651d7a880838..1a8f87f9c0b2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,8 @@ dependencyResolutionManagement { } mavenCentral() maven("https://jitpack.io") + maven("https://nexus.scanbot.io/nexus/content/repositories/releases/") + } } @@ -67,4 +69,6 @@ includeBuild("../android_common") { } */ -include(":app", ":appscan") +//} + +include(":app", ":appscan", ":scanbot") \ No newline at end of file From b1b1158ed6d687d81a35f1325c6d6de8d2d96ac6 Mon Sep 17 00:00:00 2001 From: Alex Kucherenko Date: Tue, 13 Oct 2026 23:54:58 +0300 Subject: [PATCH 2/3] Release 1.4.0 --- .idea/copyright/profiles_settings.xml | 2 +- app/build.gradle.kts | 6 +++++- scanbot/jacoco.exec | Bin 1838 -> 0 bytes 3 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 scanbot/jacoco.exec diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index 2e31899f7ffd..bc106e52608a 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8419828be50f..7febff960bd3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -156,7 +156,9 @@ android { register("gplay") { applicationId = "com.ionos.hidrivenext" dimension = "default" - versionCode = 18 + versionCode = 19 + val releaseNumber = 4 + versionName = "$versionMajor.$versionMinor.$versionPatch.$releaseNumber" isDefault = true resConfigs("en", "de", "es", "fr", "it", "en-rUS", "es-rUS", "en-rCA", "fr-rCA", "es-rMX") } @@ -167,6 +169,8 @@ android { applicationId = "com.strato.hidrivenext" dimension = "default" versionCode = 1 + val releaseNumber = 1 + versionName = "$versionMajor.$versionMinor.$versionPatch.$releaseNumber" resConfigs("en", "de", "es", "fr", "it") } diff --git a/scanbot/jacoco.exec b/scanbot/jacoco.exec deleted file mode 100644 index 24de5bfe7ff3e32150bfbaf9791d60226c264ec4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1838 zcmZQPa6o`vfI-tA~vFX64 zf-neO_(gDq_Ove%Deb!%B9ilS^~*AgQ_E8G^izOjPJV%YadKi_QhrHJW>QgNQKf!S zYD#GZOfWt#u?0WOfJdH&r=D=OfD%cN)1jdOHE;B7u+sh<#u;%T`fa6 zH7yCrPfyQDb<0UC&R}8|Oz5d#X-_chX7Hnm9lrS~sW~dfDuyilf;ldXuF+Z@dJH~P zG}?rPM=*$qao9{1jpcriw+d z_;o7HOUX$Uw`X7w+*_Ko+fn|G43)wt2$<56Q;Wn_{x=ug^zlzwwdHqyDj5xlSwmKN z!EVW00jDA_A7t>RN~l26J}@b6vU1VgJZI}KDknu_pp(x#Y~3);c;ZJY8*Kzk<<&Kp z1(=R>R5L_UF*HWAW!J5IwAWx zNmNZIM!-NnGGpuYrE^zmGbB;P&CoIuQhYg?stNyNZ?G2Z zUtl3)Gco8ARm0kt Date: Fri, 16 Oct 2026 14:18:17 +0300 Subject: [PATCH 3/3] Release 1.4.0 --- app/build.gradle.kts | 9 ++++++++- gradle/libs.versions.toml | 2 ++ gradle/verification-metadata.xml | 20 +++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7febff960bd3..e67b0a8bbf97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,6 +21,7 @@ val perfAnalysis = project.hasProperty("perfAnalysis") plugins { alias(libs.plugins.android.application) + alias(libs.plugins.google.services) alias(libs.plugins.kotlin.compose) alias(libs.plugins.spotless) alias(libs.plugins.kapt) @@ -156,7 +157,7 @@ android { register("gplay") { applicationId = "com.ionos.hidrivenext" dimension = "default" - versionCode = 19 + versionCode = 20 val releaseNumber = 4 versionName = "$versionMajor.$versionMinor.$versionPatch.$releaseNumber" isDefault = true @@ -286,6 +287,12 @@ spotless.kotlin { detekt.config.setFrom("detekt.yml") +// Firebase config comes from google-services.json; flavors without the file keep their Firebase strings +googleServices { + missingGoogleServicesStrategy = + com.google.gms.googleservices.GoogleServicesPlugin.MissingGoogleServicesStrategy.WARN +} + if (shotTest) configure { showOnlyFailingTestsInReports = ciBuild // CI environment renders some shadows slightly different from local VMs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 652e246d0563..b251b43e9851 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ firebaseAnalyticsVersion = "23.0.0" flexboxVersion = "3.0.0" fragmentKtxVersion = "1.8.9" glide = "5.0.7" +googleServicesGradlePlugin = "4.5.0" gsonVersion = "2.14.0" ical4jVersion = "3.2.19" jackrabbitWebdavVersion = "2.13.5" @@ -304,6 +305,7 @@ gplay = [ [plugins] android-application = { id = "com.android.application", version.ref = "androidPluginVersion" } android-library = { id = "com.android.library", version.ref = "androidPluginVersion" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServicesGradlePlugin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsGradlePlugin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c8c451472787..60937efb9850 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20,6 +20,7 @@ + @@ -61,6 +62,7 @@ + @@ -456,7 +458,10 @@ - + + + + @@ -23813,6 +23818,14 @@ + + + + + + + + @@ -32856,6 +32869,11 @@ + + + + +